- 医疗健康
- 图像处理
- 数据可视化
- 桌面应用
- 3D渲染
【免费下载链接】Slicer
Multi-platform, free open source software for visualization and image computing.
本篇面向 Slicer 开发者,聚焦DICOM模块的底层实现与二次开发要点:理解 CTK 数据库 / DIMSE 网络层与 DICOM 插件架构的分工,掌握自定义浏览器表格列、显示字段生成规则、注册自定义 DICOM 插件的方法,并能调用 DCMTK 命令行工具、dicomweb-client与DICOMLib脚本库完成导入、标签访问与网络收发。读完你应能独立完成 DICOM 插件扩展、浏览器定制与 PACS 交互脚本。
Slicer 用户指南中的 DICOM 章节 面向使用者介绍了标准、数据库、插件与网络功能的全景;本文以开发者视角切入,围绕 DICOM 模块开发文档 展开,并用仓库源码印证每一处机制。
一、总体架构:CTK 数据库与 DIMSE 网络层,加上 DICOM 插件钩子
Slicer 中的 DICOM 实现由两部分代码构成,这一分工决定了开发时“数据库 / 网络”与“数据解释 / 场景接入”各自落在哪里:
- CTK 代码:负责 DICOM 数据库(基于 SQLite 的本地索引)与 DIMSE 网络层的实现,用 C++ 编写,遵循 Qt 风格。数据库中缓存了每个文件的顶层 DICOM 标签,使得后续按标签检索无需重新解析文件头。
- DICOM 模块:把 CTK 能力暴露给 Slicer 用户,并提供“钩子(hooks)”,让其他模块可以注册 DICOM 插件来处理特定 DICOM 数据对象到对应 MRML 表示的转换。数据一旦进入 Slicer,就可以通过标准 Slicer 机制(MRML 节点、Subject Hierarchy、IO)继续操作。
从源码结构看,这一架构在仓库中的落点很清晰:
- 模块主体是脚本模块 DICOM.py。其中
initializeDICOMDatabase()把slicer.app.dicomDatabase()赋给slicer.dicomDatabase,并聚合所有已注册插件的标签用于预缓存——这是 DICOM 快速加载的关键:导入时未预缓存的标签会在 Examine 阶段被逐个读取,性能差多个数量级。相关代码遍历slicer.modules.dicomPlugins并把每个插件的plugin.tags值并入slicer.dicomDatabase.tagsToPrecache。 - 插件基类、加载/导出容器、数据库/网络工具集中在脚本库 DICOMLib,插件实现位于 DICOMPlugins。
数据库位置与打开
从initializeDICOMDatabase()可以看到:数据库目录按 CTK 的schemaVersion()存入 QSettings(键为DatabaseDirectory_<schemaVersion>),数据库文件固定为<目录>/ctkDICOM.sql;自动测试时则使用独立的临时数据库目录。这也解释了用户指南中“数据库只是本地磁盘上的一个目录(含若干 SQLite 文件与子目录),不要手工修改”这一说法。
二、Python 脚本开发可用的三类 DICOM 工具
原文档指出,除模块本身外,脚本化开发还可用三套工具,下面逐一给出源码级入口。
1. DCMTK 命令行工具(DICOMCommand)
随 Slicer 安装的 DCMTK 命令行工具可在脚本模块中同步调用。其封装在 DICOMProcesses.py 的DICOMCommand类中:构造时拼接exeDir下的可执行文件名,start()会追加 TLS 参数、启动进程并waitForFinished(),阻塞直到工具返回,最后返回标准输出;进程崩溃或退出码非 0 会抛出UserWarning。典型用法(把 PNG 转为 DICOM):
from DICOMLib import DICOMCommand command = DICOMCommand('img2dcm', ['image.png', 'output.dcm']) stdout = command.start() # 同步运行,阻塞直到 img2dcm 返回2. dicomweb-client(DICOMweb 网络)
Python 模块dicomweb-client内置于 Slicer,用于 DICOMweb 网络。仓库中它被DICOMUtils.importFromDICOMWeb()直接驱动(见下文网络小节)。
3. DICOMLib 脚本模块
Slicer 特有的 DICOM 管理与网络实现集中在 DICOMLib。其中最常用的 DICOMUtils 提供了导入、按 UID/姓名/PatientID 加载、临时数据库、DICOMweb 导入、标签批量读取等函数,是本节和后续插件示例的基础。
三、自定义 DICOM 浏览器的表格列
DICOM 浏览器(Patient / Study / Series 三张表)的列可以重命名、重排、显隐。改动会被写入数据库的ColumnDisplayProperties表,因此每个数据库只需做一次定制即可持久生效。
以下脚本来自脚本仓库,展示如何取得浏览器、列出可用列并调整顺序 / 可见性 / 名称 / 宽度,最后关闭再重开数据库以强制刷新视图:
# 取浏览器与数据库 dicomBrowser = slicer.modules.dicom.widgetRepresentation().self().browserWidget.dicomBrowser dicomDatabase = dicomBrowser.database() # 打印可用列 print(dicomDatabase.patientFieldNames) print(dicomDatabase.studyFieldNames) print(dicomDatabase.seriesFieldNames) # 调整列顺序(weight) dicomDatabase.setWeightForField("Series", "SeriesDescription", 7) dicomDatabase.setWeightForField("Studies", "StudyDescription", 6) # 调整列可见性 dicomDatabase.setVisibilityForField("Patients", "PatientsBirthDate", False) dicomDatabase.setVisibilityForField("Patients", "PatientsComments", True) dicomDatabase.setWeightForField("Patients", "PatientsComments", 8) # 重命名列 dicomDatabase.setDisplayedNameForField("Series", "DisplayedCount", "Number of images") # 手动调整某列宽度(interactive = 可拖动) dicomDatabase.setFormatForField("Series", "SeriesDescription", '{"resizeMode":"interactive"}') # 定制 DICOM 浏览器内的表格管理器 dicomTableManager = dicomBrowser.dicomTableManager() dicomTableManager.selectionMode = qt.QAbstractItemView.SingleSelection dicomTableManager.autoSelectSeries = False # 强制刷新数据库视图 dicomDatabase.closeDatabase() dicomDatabase.openDatabase(dicomBrowser.database().databaseFilename)完整说明与更多变体见 脚本仓库 DICOM 部分。
四、自定义 DICOM 浏览器“显示内容”(显示字段生成规则)
浏览器表格里每个字段如何从原始 DICOM 标签生成,由**显示字段生成规则(displayed field generator rules)决定。这些规则是ctkDICOMDisplayedFieldGeneratorAbstractRule的子类,并且必须注册(register)**到 displayed field generator 才会参与生成。
机制要点(对照 CTK 头文件ctkDICOMDisplayedFieldGenerator.h/...AbstractRule.h/ctkDICOMDatabase.h):
- 何时更新:显示字段在 1) 文件加入数据库,或 2) 数据库 schema 更新(用新版本 Slicer 打开旧数据库时会触发)时被更新。当前实现中只处理“从未生成过显示字段”的那些文件(实例)。
- 规则职责声明:每条规则用数据库里缓存的 DICOM 标签来声明它负责哪些字段;标签通过规则的
getRequiredDICOMTags请求缓存。 - 逐实例生成再合并:更新显示字段时,先对每条规则调用
getDisplayedFieldsForInstance(从原始标签生成自定义值),再对所有规则调用mergeDisplayedFieldsForInstance合并结果。每个字段可指定合并策略:expect same value:只取唯一非空值,遇到冲突值时抛出警告;concatenate:直接把各规则的显示字段值拼接起来。
- 参考实现:CTK 内置了两条可直接参考的规则——
default规则和RT(放射治疗序列描述)规则。
对开发者而言,扩展浏览器显示内容的正确姿势是:继承该抽象规则类、在getRequiredDICOMTags声明所需标签、在getDisplayedFieldsForInstance生成字段,并注册进 generator,从而参与合并。
五、插件架构:把 DICOM 数据对象映射为 MRML
DICOM 标准信息对象种类很多,全部放进 Slicer 核心并不现实(参见 用户指南中的 DICOM 插件说明)。因此扩展可以基于DICOMLib.DICOMPlugin实现自定义导入/导出类,并加入slicer.modules.dicomPlugins字典;DICOM 模块会依据这些插件决定可加载(loadable)与可导出(exportable)项。
5.1 基类与核心方法
DICOMPlugin 定义了插件需要覆盖的接口,以及一组可复用的辅助方法:
examineForImport(fileList)/examineFiles(files):审查一组文件并返回可加载项列表。默认实现examineForImport会按文件组做缓存(用文件列表的 MD5 作键,见getCachedLoadables/cacheLoadables/hashFiles),子类可覆盖examineFiles()复用缓存,或覆盖examineForImport()绕过缓存。旧的examine()仅为向后兼容。load(loadable):把选中的DICOMLoadable转成 MRML 节点,返回节点;子类必须覆盖。examineForExport(subjectHierarchyItemID)/export(exportable):导出方向,返回/处理DICOMExportable。addSeriesInSubjectHierarchy(loadable, dataNode):加载后把 series 挂到 Subject Hierarchy 的 Patient / Study / Series 三层,并写入DICOM.*属性(modality、seriesNumber、frameOfReferenceUID、日期、患者信息等)。mapSOPClassUIDToModality(sopClassUID)与mapSOPClassUIDToDICOMQuantityAndUnits(sopClassUID):把 SOP Class UID 映射为模态(MR/CT/PT)及带编码的测量量与单位(如 CT 的 Hounsfield unit、MR 信号强度),供结构化报告 / 参数化图等使用。isDetailedLogging():读取DICOM/detailedLogging设置,允许插件在开启详细日志时输出更多调试信息。
5.2 加载项与导出项
- 加载项容器
DICOMLoadable携带files、name、tooltip、warning、selected、confidence等字段。confidence取值 0~1,表示该插件“这是最可能想要的解释”的把握;当多个插件对同一 series 都标记 selected 时,置信度最高者被默认选中,平手则都选中。选择逻辑见 DICOMUtils.selectHighestConfidenceLoadables。 - 导出项
DICOMExportable在 C++ 侧由 vtkSlicerDICOMExportable 实现,字段包括Name、Tooltip、SubjectHierarchyItemID、PluginClass、Directory、Confidence,以及一张**伪标签(pseudo-tags)**表Tags:这些“标签”在导出时才会被插件翻译为真实 DICOM 标签,并允许插件暴露压缩、命名约定等额外导出选项。
5.3 内置插件与注册模式
内置脚本插件在 DICOMPlugins/CMakeLists.txt 中列明:DICOMScalarVolumePlugin、DICOMSegmentationPlugin、DICOMParametricMapPlugin、DICOMVolumeSequencePlugin、DICOMImageSequencePlugin、DICOMEnhancedUSVolumePlugin、DICOMGeAbusPlugin、DICOMM3DPlugin、DICOMSlicerDataBundlePlugin、DICOMTID1500Plugin,以及(在多卷支持开启时)MultiVolumeImporterPlugin。
每个插件文件末尾都会把自己登记进slicer.modules.dicomPlugins,这是“扩展如何接入模块”的关键动作,例如 DICOMScalarVolumePlugin.py:
if not hasattr(slicer.modules, "dicomPlugins"): slicer.modules.dicomPlugins = {} slicer.modules.dicomPlugins["DICOMScalarVolumePlugin"] = DICOMScalarVolumePluginClassDICOMScalarVolumePlugin还展示了插件如何向“应用程序设置 / DICOM”面板贡献配置项:settingsPanelEntry()注册了DICOM/ScalarVolume/ReaderApproach(GDCM / DCMTK / GDCM with DCMTK fallback / Archetype,其中 fallback 为默认)与DICOM/ScalarVolume/AcquisitionGeometryRegularization(default / none / transform / hardenTransform,后者需重启生效)。
扩展生态方面,DICOMExtensions.json 声明了若干外部扩展提供的插件能力及其匹配的Modality/SOPClassUID标签值(如 SlicerRT 的RTDOSE/RTSTRUCT/RTPLAN/RTIMAGE、SlicerHeart 的超声 SOP、PETDICOMExtension 的PT/RWVM)。
5.4 一次“加载”的完整调用链
从 DICOMUtils 可拼出脚本化加载的完整链路:
importDicom(dicomDataDir, dicomDatabase):用ctkDICOMIndexer把目录加入数据库并waitForImportFinished()。getLoadablesFromFileLists(fileLists, ...):对每个启用的插件实例化并调用examineForImport,产出loadablesByPlugin。selectHighestConfidenceLoadables(...):按 series 选出最高置信度可加载项。loadLoadables(loadablesByPlugin):对每个 selected loadable 调plugin.load(loadable),并观察场景NodeAddedEvent/NodeRemovedEvent收集新节点 ID,把结果写回loadable.loadSuccess。
对外的高层便捷函数loadPatientByUID/loadPatientByName/loadPatientByPatientID/loadSeriesByUID/loadByInstanceUID都收敛到这条链。
六、DICOM 网络开发
Slicer 结合 CTK、DCMTK 与dicomweb-client提供 DICOM 网络能力:存储在远程 PACS 的图像可被查询、取回、在 Slicer 中标注后再写回。
6.1 本地测试环境
原文档建议开发者为不同需求搭建本地测试环境:
DIMSE 测试:用随 Slicer 分发的 DCMTK
storescp测试异步 C-STORE 请求。在 shell 中运行:"path/to/Slicer x.y.z/bin/storescp.exe" <port-number>然后在 Slicer 里把 DICOM 数据发送到
localhost:<port-number>。DICOMweb 测试:可本地部署开源 PACS(如 Orthanc、DCM4CHE)在隔离环境中验证。
6.2 仓库中的网络实现
- C-STORE SCP(接收):DICOM.py 的
startListener()用 DICOMListener 启动接收器;底层 DICOMStoreSCPProcess 运行 DCMTK 的storescp,默认端口 11112(StoragePort),带--accept-all、--output-directory与“收到即回调dcmdump打印摘要”的逻辑,并在启动前清理已存在的storescp进程。 - 发送(C-STORE SCU):DICOMSender 支持两种协议:
DIMSE:逐文件调用 DCMTKstorescu(分段对象使用storescu-seg.cfg配置);DICOMweb:通过dicomweb_client的 STOW-RS 发送,可对 Kheops 分享链接做特殊解析,并接受requests.auth.AuthBase认证。- 注意:HTTP 认证(
auth)仅对 DICOMweb 有效,DIMSE 会给出警告。delayed=True时不立即发送,构造后手动send()。
- 查询 / 检索(C-FIND / C-MOVE / C-GET):脚本中可用
ctk.ctkDICOMQuery+ctk.ctkDICOMRetrieve组合完成查询与取回;ctkDICOMVisualBrowser与ctkDICOMScheduler(无 UI)也提供了面向服务器列表的查询/检索与任务跟踪能力。 - DICOMweb 导入:DICOMUtils.importFromDICOMWeb 用
dicomweb_client.api.DICOMwebClient先search_for_series找到 series,再逐 seriesretrieve_series_metadata判断是否已导入(按 SOP Instance UID 去重),随后iter_series(bulk)或逐实例retrieve_instance下载,写入数据库目录下的DICOMweb/<时间戳>临时目录并importDicom。支持accessToken(Bearer)或auth(AuthBase,如HTTPBasicAuth),二者都给定时优先用 AuthBase。
6.3 常用网络脚本片段
以下来自 脚本仓库 DICOM 部分,可直接粘贴到 Python 控制台。
从文件夹加载到场景(用临时数据库,不影响当前数据库):
dicomDataDir = "c:/my/folder/with/dicom-files" loadedNodeIDs = [] from DICOMLib import DICOMUtils with DICOMUtils.TemporaryDICOMDatabase() as db: DICOMUtils.importDicom(dicomDataDir, db) for patientUID in db.patients(): loadedNodeIDs.extend(DICOMUtils.loadPatientByUID(patientUID))用 DICOMweb 从 PACS 导入(含访问令牌 / 基础认证 / 全局认证):
slicer.util.selectModule("DICOM") # 确保数据库已初始化 slicer.app.processEvents() from DICOMLib import DICOMUtils DICOMUtils.importFromDICOMWeb( dicomWebEndpoint="https://demo.kheops.online/api", studyInstanceUID="1.3.6.1.4.1.14519.5.2.1.8421.4009.985792766370191766692237040819") # 使用 requests.HTTPBasicAuth 作为替代认证 # DICOMUtils.importFromDICOMWeb(..., auth=requests.HTTPBasicAuth('<user>','<password>')) # 配置跨会话记忆的全局认证 qt.QSettings().setValue(DICOMUtils.GLOBAL_DICOMWEB_USER_KEY, '<user>') qt.QSettings().setValue(DICOMUtils.GLOBAL_DICOMWEB_PASSWORD_KEY, '<pwd>') DICOMUtils.importFromDICOMWeb( dicomWebEndpoint="https://remote-url/", studyInstanceUID="1.3.6.1.4.1.14519.5.2.1.8421.4009.985792766370191766692237040819", auth=DICOMUtils.getGlobalDICOMAuth())用经典 DIMSE 从 PACS 查询并检索(useCGET=True用 C-GET,简单但很多临床 PACS 不开放;关闭后 PACS 会用 C-STORE 把数据推给 Slicer,需要 Slicer 的 C-STORE SCP 在运行且 AE Title 已在 PACS 侧配置):
dicomQuery = ctk.ctkDICOMQuery() dicomQuery.callingAETitle = "SLICER" dicomQuery.calledAETitle = "ANYAE" dicomQuery.host = "dicomserver.co.uk" dicomQuery.port = 11112 dicomQuery.setFilters({"Name": "A", "Modalities": "MR"}) tempDb = ctk.ctkDICOMDatabase() tempDb.openDatabase("") dicomQuery.query(tempDb) useCGET = True dicomRetrieve = ctk.ctkDICOMRetrieve() dicomRetrieve.callingAETitle = dicomQuery.callingAETitle dicomRetrieve.calledAETitle = dicomQuery.calledAETitle dicomRetrieve.host = dicomQuery.host dicomRetrieve.port = dicomQuery.port dicomRetrieve.setDatabase(slicer.dicomDatabase) for study, series in dicomQuery.studyAndSeriesInstanceUIDQueried: slicer.app.processEvents() if useCGET: success = dicomRetrieve.getSeries(study, series) else: dicomRetrieve.moveDestinationAETitle = dicomQuery.callingAETitle success = dicomRetrieve.moveSeries(study, series) slicer.dicomDatabase.updateDisplayedFields()发送数据到 PACS(DIMSE 与 DICOMweb):
from DICOMLib import DICOMSender # DIMSE sender = DICOMSender(files=['path/to/0.dcm', 'path/to/1.dcm'], address='dicomserver.co.uk:9999', protocol="DIMSE", delayed=True) sender.send() # DICOMweb sender = DICOMSender(files=['path/to/0.dcm', 'path/to/1.dcm'], address='dicomserver.co.uk:9999', protocol="DICOMweb", auth=DICOMUtils.getGlobalDICOMAuth(), delayed=True) sender.send()七、标签访问与导出脚本
7.1 读取标签(顶层与序列内嵌套)
db.fileValue读取的是缓存的顶层标签(不含嵌套序列);嵌套标签需要用pydicom.dcmread重新读文件头。
db = slicer.dicomDatabase patientList = db.patients() studyList = db.studiesForPatient(patientList[0]) seriesList = db.seriesForStudy(studyList[0]) fileList = db.filesForSeries(seriesList[0]) print(db.fileValue(fileList[0], "0020,0032")) # 顶层标签 import pydicom ds = pydicom.dcmread(fileList[0]) ds.CTExposureSequence[0].ExposureModulationType # 序列内嵌套标签从已加载节点反查标签(不同插件保存 UID 的方式不同):DICOMScalarVolumePlugin把SOP Instance UIDs存到 volume 节点的DICOM.instanceUIDs属性;其他插件可能只把Series Instance UID存到 Subject Hierarchy 项上,需再经instancesForSeries换取实例。脚本仓库给出了两类反查示例(按节点GetAttribute("DICOM.instanceUIDs")与按shNode.GetItemUID(itemID, 'DICOM'))。
7.2 导出:体数据与分割对象
导出走“Subject Hierarchy 项 →examineForExport→ 设置directory与伪标签 →export”的路径,与加载侧的 loadable 对称:
# 导出标量体到 DICOM 文件 volumeNode = slicer.util.getNode("CTChest") outputFolder = "c:/tmp/dicom-output" shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) patientItemID = shNode.CreateSubjectItem(shNode.GetSceneItemID(), "test patient") studyItemID = shNode.CreateStudyItem(patientItemID, "test study") volumeShItemID = shNode.GetItemByDataNode(volumeNode) shNode.SetItemParent(volumeShItemID, studyItemID) import DICOMScalarVolumePlugin exporter = DICOMScalarVolumePlugin.DICOMScalarVolumePluginClass() exportables = exporter.examineForExport(volumeShItemID) for exp in exportables: exp.directory = outputFolder exp.setTag('PatientID', "test patient") exp.setTag('StudyID', "test study") exporter.export(exportables)分割对象导出到 DICOM Segmentation 的写法见 脚本仓库(关联分割节点与参考体节点到同一 study 项,再examineForExport+ 设置directory+export)。批量把整个数据库导出为 NRRD/NIfTI 的脚本也在其中,可按DICOM.PatientID/DICOM.StudyDate/DICOM.SeriesNumber/DICOM.Modality等 Subject Hierarchy 属性构造文件名。
八、参考与延伸阅读
- DICOM 实现内部细节以 CTK 与 DCMTK 为准(
ctkDICOMDatabase.h、ctkDICOMDisplayedFieldGenerator*.h、ctkDICOMDisplayedFieldGeneratorAbstractRule.h等头文件为权威定义)。 - 常见 DICOM 操作示例(导入、标签访问、导出、查询/检索、DCMTK 工具调用)汇总在 脚本仓库 DICOM 部分。
- 使用者侧功能与面板说明见 DICOM 用户指南。
本文所有源码路径均相对仓库根目录,便于按路径继续深入对应实现。
- 医疗健康
- 图像处理
- 数据可视化
- 桌面应用
- 3D渲染
【免费下载链接】Slicer
Multi-platform, free open source software for visualization and image computing.
相关推荐
OpenMediaVault终极开发者指南:模块化架构与插件开发全解析
OpenMediaVault终极开发者指南:模块化架构与插件开发全解析 OpenMediaVault是一款基于Debian Linux的下一代网络附加存储(NA
存储后端前端Halo 插件开发:Entity 数据列表显示字段扩展点(EntityFieldItem)完整指南
Halo 插件开发:Entity 数据列表显示字段扩展点(EntityFieldItem)完整指南 导读 本文讲解 Halo 2 Console 中「Entit
后端前端CMSCATS开发者指南:深入理解插件架构与扩展开发
CATS开发者指南:深入理解插件架构与扩展开发 CATS Blender Plugin 是一款专为简化 VRChat 模型导入和优化流程而设计的强大工具,支持
图形学游戏开发数字人
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考