OHIF HangingProtocolService 深度解析:挂片协议的匹配引擎、事件机制与自定义属性实战

发布时间:2026/9/18 15:57:59
OHIF HangingProtocolService 深度解析:挂片协议的匹配引擎、事件机制与自定义属性实战 OHIF HangingProtocolService 深度解析挂片协议的匹配引擎、事件机制与自定义属性实战【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/ViewersOHIFZero-Footprint DICOM Viewer通过HangingProtocolServiceHP Service实现影像在视口中的自动化排布注册到服务中的各挂片协议Hanging Protocol会与当前可用的 DisplaySet 逐一匹配、按得分排序胜出协议的配置随后被应用到视图布局中。本文基于仓库文档 HangingProtocolService.md 与核心实现 HangingProtocolService.ts完整讲解该服务的协议注册结构、事件模型、阶段激活状态机、API 面以及自定义属性Custom Attribute的两种实战用法帮助你在自研扩展或模式中正确驱动 OHIF 的视口编排。一、工作原理协议打分与胜出者应用HP Service 的核心职责是“把正确的 DisplaySet 挂到正确的视口上”。按文档描述流程为各扩展通过自身的 HangingProtocolModule 向服务注册协议服务将注册协议与当前可用的 DisplaySet 进行匹配为每个协议计算得分得分最高的协议胜出其stages配置布局、视口类型、初始图像选项、同步组等被用于排布视口。协议结构与属性的完整定义参见 HangingProtocol Module 文档类型定义则位于 HangingProtocol.ts。从源码看HangingProtocolService.tsrun()方法是整个流程的入口public run({ studies, displaySets, activeStudy }, protocolId, options {}) { this.studies [...(studies || this.studies)]; this.displaySets displaySets; this.setActiveStudyUID(...); this.protocolEngine new ProtocolEngine( this.getProtocols(), this.customAttributeRetrievalCallbacks ); if (protocolId typeof protocolId string) { const protocol this.getProtocolById(protocolId); this._setProtocol(protocol, options); } else { const matchedProtocol this.protocolEngine.run({ studies: this.studies, activeStudy, displaySets, }); this._setProtocol(matchedProtocol); } // ...触发协议自身的 onProtocolEnter 回调 }可以看到两条路径显式传入protocolId时直接取用该协议跳过打分否则交由ProtocolEngine实现于 ProtocolEngine.js在全部已注册协议中搜索最佳匹配。服务通过REGISTRATION静态属性注册到服务管理器名称为hangingProtocolService别名HangingProtocolServicepublic static REGISTRATION { name: hangingProtocolService, altName: HangingProtocolService, create: ({ configuration {}, commandsManager, servicesManager }) { return new HangingProtocolService(commandsManager, servicesManager); }, };二、协议Protocol如何注册文档明确指出协议由各扩展的 HangingProtocolModule 提供并自动注册到 HangingProtocolService所有协议以id为键、协议本体为值存储在服务内部。对应源码中的protocols: Mapstring, Protocol字段。模块定义结构协议在扩展内部的getHangingProtocolModule中定义模块结构以id开头protocol字段承载真正的协议定义。这种“模块包裹协议”的写法允许一个模块声明多个协议每个协议各自有独立的定义文件import MyProtocol from ./MyProtocol; export default function getHangingProtocolModule() { return [ { id: MyProtocol.id, protocol: MyProtocol, }, ]; }仓库中即可在 extensions/tmtv/src/getHangingProtocolModule.ts 等扩展入口找到符合该结构的真实实现。协议内部结构则遵循 HangingProtocol.ts 中的类型定义Protocol、ProtocolStage、DisplaySetSelector等文档建议读者直接查阅类型文件获取细节。一个值得注意的实现细节addProtocol允许“同 id 覆盖注册”这正是可以替换default协议的机制源码public addProtocol(protocolId: string, protocol: Protocol): void { if (this.protocols.has(protocolId)) { console.warn(A protocol with id ${protocolId} already exists. It will be overwritten.); } // 非函数型协议会先经过 _validateProtocol 校验 this.protocols.set(protocolId, protocol); }_validateProtocol同时承担“结构补全”职责若 stage 未显式给出viewports会依据viewportStructure.properties的rows * columns自动按行优先生成等量视口并对缺失的viewportId做default/uuid 填充源码。三、事件模型HP Service 会广播若干事件供 UI 层订阅。文档列出的核心事件及源码中对应的字符串常量如下事件源码常量HangingProtocolService.ts说明NEW_LAYOUTevent::hanging_protocol_new_layout当 HP Service 请求一个新布局时触发源码注释中标记为 deprecated 语义PROTOCOL_CHANGEDevent::hanging_protocol_changed挂片协议被更换或应用阶段stage发生变化时触发RESTORE_PROTOCOL源码常量名PROTOCOL_RESTOREDevent::hanging_protocol_restore协议或阶段被“恢复”而非重新应用时触发例如关闭 MPR 模式后回到原有协议STAGE_ACTIVATIONevent::hanging_protocol_stage_activation当各 stage 的status已知激活/停用判定完成时触发从源码看_setProtocol在成功应用协议后根据options.restoreProtocol二选一广播if (options?.restoreProtocol ! true) { this._broadcastEvent(HangingProtocolService.EVENTS.PROTOCOL_CHANGED, { viewportMatchDetails, displaySetMatchDetails, protocol, stageIdx, stage, activeStudyUID, }); } else { this._broadcastEvent(HangingProtocolService.EVENTS.PROTOCOL_RESTORED, { ... }); }NEW_LAYOUT则在_updateViewports中携带layoutType / numRows / numCols / layoutOptions广播STAGE_ACTIVATION在阶段状态更新完成后携带protocol与stages数组广播。此外源码中还定义了CUSTOM_IMAGE_LOAD_PERFORMED事件与下文registerImageLoadStrategy的自定义图像加载策略联动。四、阶段激活Stage Activation与状态有些挂片协议在语义上普遍适用但并非所有 stage 都应当默认展示。HP Service 用 stage 激活机制处理这种情况enabledstage 完全适用默认展示passivestage 可被导航到但可能缺少部分细节不默认展示disabled当前检查信息不足以支撑该 stage完全不展示。激活判定由stage.stageActivation属性控制。未显式配置时的默认规则为enabled要求minViewportsMatched至少为 1至少一个视口被填充即启用该 stagepassive的minViewportsMatched为 0即使没有视口被填充也保持可用允许用户手动拖拽填充后复用到其他 stage。文档给出的完整配置示例保留注释stageActivation: { // enabled 指定“让 stage 成为首选”所需满足的条件 enabled: { // 默认值为 1表示需要多少个非空白视口 minViewportsMatched: 3, // 支持跨维度约束例如仅对男性/女性生效 // 值是 display set selector 的 id 列表 displaySetSelectorsMatched: [dsMale], }, // passive 检查先执行若失败则不检查 enabled直接置为 disabled。 // 默认 passive 检查总是通过因此通常只需定义 enabled。 passive: { // 默认 0即使没有视口被填充也允许该 stage。 // 这使得可以先拖拽填充视口做手工匹配再复用到其他 stage。 minViewportsMatched: 0, displaySetSelectorsMatched: [...], }, }_updateStageStatus的判定顺序与文档完全一致源码if (this.matchActivation(matchedViewports, activation.passive, 0)) { if (this.matchActivation(matchedViewports, activation.enabled, 1)) { stage.status enabled; } else { stage.status passive; } } else { stage.status disabled; }matchActivation内部先检查displaySetSelectorsMatched中每个 selector 是否存在最优匹配再比较matchedViewports与minViewportsMatched。状态确定后会立刻广播STAGE_ACTIVATION事件。后续选择初始 stage 时_findStageIndex优先选第一个enabled的 stage找不到则回退到第一个非disabled的 stage这解释了为何 passive stage 仍可被用户手动切换。五、完整 API 面以下为文档列出的全部 API并附源码中的签名与行为要点destroy销毁 HP Service内部调用reset并清空协议表。reset/onModeEnter将服务重置为“无活动挂片协议”的初始状态模式初始化时会经由onModeEnter触发。getActiveProtocol返回 HP Service 的内部状态对象包含protocol、_originalProtocol未被自定义污染的协议副本、stage、stageIndex、activeStudy、viewportMatchDetails、displaySetMatchDetails、activeImageLoadStrategyName。文档明确提醒该返回值不保证长期稳定内部细节可能变化适合用于状态暂存或异常恢复不适合作为长期持久化格式。getState返回当前应用的协议 id、stage 下标与活动检查 UID即{ protocolId, stageIndex, stageId, activeStudyUID }——这是设计为可安全存储/传递的轻量状态。getDefaultProtocol返回 id 为default的协议。getMatchDetails返回视口与 DisplaySet 的匹配明细viewportMatchDetails/displaySetMatchDetails。文档标记其为deprecated——匹配结果未来将以事件形式对外通知。getProtocols返回当前活动的协议列表注意实现上只遍历activeProtocolIds未设置时才是全部协议且会自动执行函数型协议generator。getProtocolById按 id 取协议支持大小写不敏感回退查找若协议是函数generator则执行并返回生成的协议对象找不到时抛出No protocol ${protocolId} found。addProtocol将协议加入注册表参与匹配同 id 会覆盖可借此替换 default 协议。setActiveProtocolIds文档中称setActiveProtocols的语义选择哪些协议处于活动状态可传单个 id 或 id 列表传入单个时该协议将无论匹配规则是否满足都被应用在 mode 初始化时自动调用。setActiveStudyUID设置活动检查 UID匹配规则可以针对活动检查做匹配如from: activeStudy。run({ studies, activeStudy, displaySets }, protocolId)用给定的 study/displaySet 数据运行 HP Service不传protocolId时匹配引擎会在全部已注册协议中按约束搜索最佳匹配。registerImageLoadStrategy注册自定义图像加载策略协议侧以imageLoadStrategy字段按名称引用可用内置策略如interleaveTopToBottom、interleaveCenter、nth详见 Hanging Protocol Module 文档。addCustomAttribute注册自定义匹配属性见下文第六节。setProtocol对当前检查直接应用指定协议——例如点击工具栏 MPR 按钮时强制应用 MPR 协议。可接受options参数定义协议使用的 displaySets不提供 options 时使用全部 displaySets 参与匹配。实现上源码options.displaySetSelectorMap支持按${activeStudyUID}:${selectorId}:${matchedDisplaySetsIndex}键指定具体实例restoreProtocol: true则走PROTOCOL_RESTORED事件路径。getStageIndex根据给定的一组匹配键stageId或stageIndex查找 stage 下标文档说明目前只作用于当前活动协议设计上应支持任意协议。getMissingViewport返回一个用于填充新增视口的视口匹配对象查找顺序为先 stage 级defaultViewport后协议级defaultViewport。文档最后强调各 mode 的默认初始化逻辑会负责调用HangingProtocolService——即你不需要在业务代码里手动驱动 run/setProtocol除非要做模式级定制。六、自定义属性Custom Attribute实战DICOM tag 之外你可能希望按业务字段如给每个检查分配的timepointId做匹配。OHIF v3 允许注册自定义属性并用于匹配规则与视口选项。6.1 在模式配置中注册并用于匹配文档给出的完整示例在模式工厂中订阅DicomMetadataStore的SERIES_ADDED事件在检查元数据就绪后调用addCustomAttribute再触发runconst defaultProtocol { id: defaultProtocol, protocolMatchingRules: [ { weight: 3, attribute: timepoint, constraint: { equals: first, }, required: false, }, ], displaySetSelectors: { /* ... */ }, stages: [/* ... */], numberOfPriorsReferenced: -1, }; // 自定义属性的取值函数 const getTimePointUID metaData { return myBackEndAPI(metaData); // 请求后端获取 timepoint id }; function modeFactory() { return { id: myMode, routes: [ { path: myModeRoute, init: async ({}) { const { DicomMetadataStore, HangingProtocolService, } servicesManager.services; const onSeriesAdded ({ StudyInstanceUID, madeInClient false, }) { const studyMetadata DicomMetadataStore.getStudy(StudyInstanceUID); // 向 HangingProtocolService 注册自定义属性 HangingProtocolService.addCustomAttribute( timepoint, timepoint, metaData getFirstMeasurementSeriesInstanceUID(metaData) ); HangingProtocolService.run(studyMetadata); }; DicomMetadataStore.subscribe( DicomMetadataStore.EVENTS.SERIES_ADDED, onSeriesAdded ); }, }, ], }; }从源码看addCustomAttribute的签名是(attributeId, attributeName, callback, options {})源码attributeId是匹配规则中attribute字段引用的标识attributeName是展示用名称callback接收该层级的元数据study/series/displaySet返回属性值options会并入注册项并可挂在this上。这些回调统一存放在customAttributeRetrievalCallbacks并被传入ProtocolEngine参与规则求值。服务本身也内置了一组自定义属性开箱即用源码NumberOfStudyRelatedSeries检查内序列数、NumberOfSeriesRelatedInstancesdisplay set 内实例数、ModalitiesInStudy检查涉及的模态列表、isReconstructable是否可三维重建、isDisplaySetFromUrl、sopInstanceLocation、seriesDescriptions、numberOfDisplaySetsWithImages。6.2 自定义属性用于视口选项自定义属性不仅可用于匹配还可用于视口选项的动态计算。文档给出的例子来自 default 挂片协议把图像导航到 URL 中指定的那张viewportOptions: { initialImageOptions: { // 以 custom 键选择自定义属性 custom: sopInstanceLocation, // 上述属性取不到值时使用的回退值 defaultValue: { index: 5 }, }, }这背后的机制是getComputedOptions源码它对 options 做深度递归遇到{ custom, defaultValue }结构时取出customAttributeRetrievalCallbacks[custom]的回调、以当前视口关联的 displaySets 调用它回调返回undefined时回退到defaultValue并对计算结果继续递归。也就是说视口选项中的toolGroupId、viewportType、orientation、initialImageOptions等字段都可以声明为“计算型”类型定义为CustomOptionT见 HangingProtocol.ts静态声明 命名回调的组合比整体重算协议简单得多。6.3 仓库内置的示例自定义属性文档说明ohif/extension-test内置了三个自定义属性sameAsmaxNumImageFramesnumberOfDisplaySets源码印证了这一说法extensions/test-extension/src/index.tsx 在preRegistration阶段通过hangingProtocolService.addCustomAttribute注册了numberOfDisplaySets、maxNumImageFrames、sameAs另据文件头注释numberOfDisplaySetsWithImages、seriesDescriptions等也属于该扩展提供的能力集。要使用这些属性需要两步第一步在 platform/app/pluginConfig.json 中启用扩展{ extensions: [ ... { packageName: ohif/extension-test, version: 3.4.0 }, ... ] }第二步在目标模式的extensionDependencies中加入该扩展以 modes/tmtv/src/index.ts 为例const extensionDependencies { ohif/extension-default: ^3.0.0, ohif/extension-cornerstone: ^3.0.0, ohif/extension-tmtv: ^3.0.0, ohif/extension-test: ^0.0.1, };第三步在协议中引用。文档给出的例子修改了 TMTV 扩展的挂片协议extensions/tmtv/src/getHangingProtocolModule.ts使用sameAs属性校验选中的 PT 序列与 CT 具有相同的 Frame of ReferenceptDisplaySet: { // ... seriesMatchingRules: [ { attribute: sameAs, sameAttribute: FrameOfReferenceUID, sameDisplaySetId: ctDisplaySet, constraint: { equals: { value: true, }, }, required: true, }, // ... ], }其中sameAttribute指定要比对的 DICOM 属性sameDisplaySetId指定参照的另一个 display set selectorrequired: true表示不满足则整个 selector 匹配失败requiredFailed会阻止该 displaySet 成为 bestMatch见_matchImages中的短路逻辑。七、defaultViewport为新增视口提供默认配方当用户手动选择了某个尺寸如 2x3的布局时超出协议 stage 预设数量的“多余视口”用什么来填协议可以通过defaultViewport字段给出答案defaultViewport: { viewportOptions: { viewportType: stack, toolGroupId: default, allowUnmatchedView: true, }, displaySets: [ { id: defaultDisplaySetId, matchedDisplaySetsIndex: -1, }, ], },它定义了新增视口的类型、允许的工具组以及填充它的 display set。上述示例中 display set 与其他视口相同但matchedDisplaySetsIndex: -1表示“取 display set selector 中下一个尚未填充视口的匹配项”。从源码看该语义由findDeduplicatedMatchDetails实现源码offset -1且传入options.inDisplay时遍历matchingScores找到第一个不在当前显示列表中的匹配且matchingScore 0从而实现去重填充。getMissingViewport正是按“stage.defaultViewport → protocol.defaultViewport”的优先级取出该配方再走_matchViewport生成匹配细节。八、协议实例定义与 Generator 模式协议实例Hanging Protocol Instance通过模块中的id被识别。命名建议包含模块名前缀以避免冲突文档推荐的 id 形如${moduleId}.${simpleName}default这一名称在没有任何其他协议匹配时作为兜底协议使用可将其设为模块列表中最后一个含default的项。协议也可以以generator 函数的形式定义——这是一个接收{ servicesManager, commandsManager }的函数可以在生成时做任意计算function protocolGenerator({ servicesManager, commandsManager }) { // 使用 services 与 commands 做一些计算 ... return { protocol: generatedProtocol, }; }源码中对 generator 的处理见_getProtocolFromGenerator源码执行函数、取出protocol字段、送入_validateProtocol校验。getProtocolById在发现protocol instanceof Function时会执行它并捕获异常失败仅告警不中断。协议结构的完整字段说明protocolMatchingRules约束动词表、from属性、displaySetSelectors、viewportStructure、viewportOptions、callbacks等请参照 HangingProtocol Module 文档 与 HangingProtocol.ts 类型定义。九、调试与延伸阅读需要排查“协议为什么没有应用/为什么选了这个 stage”时可将服务实例的debugLogging置为true_matchImages、setCurrentProtocolStage等路径会输出 debug 日志debug(...)方法见 源码。服务行为测试可参考 HangingProtocolService.test.js。匹配引擎打分细节在 ProtocolEngine.js内置自定义属性的实现位于 custom-attribute 目录。适用前提说明以上内容基于当前仓库的platform/core源码与文档目录platform/docs/docs为准文档事件表中写作RESTORE_PROTOCOL而源码常量名为PROTOCOL_RESTORED字符串为event::hanging_protocol_restore订阅时应以源码常量为准。getMatchDetails已被标记为 deprecated新代码应改为订阅PROTOCOL_CHANGED/PROTOCOL_RESTORED事件获取匹配信息。【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考