OHIF Segmentation 模式全解析:基于 Labelmap 的分割读/编辑/导出实现与扩展指南
【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers
导读
本文以 modes/segmentation/README.md 为骨架,深入剖析 OHIF Viewers 中Segmentation 模式(@ohif/mode-segmentation)的实现原理。该模式面向医学影像的Labelmap 分割标注场景,开箱即用地提供分割数据的读取(read)、编辑(edit)与导出(export)全流程能力,并同时支持 Labelmap 与 Contour 两种分割表示。读完本文,你将掌握:Segmentation 模式与 Basic 模式的关系与差异、模式布局与视口如何组装、三套工具组的划分与画笔/擦除器/剪刀工具配置、标签图与轮廓分割面板的自动切换机制,以及如何通过modeFactory与自定义定制块快速派生属于自己的分割模式。
模式定位:专注 Labelmap 分割的独立 Mode
Segmentation 模式是 OHIF 平台中与 Basic、Longitudinal 等并列的独立模式(Mode)。按 modes/segmentation/README.md 的定义,它是"OHIF segmentation mode which enables labelmap segmentation read/edit/export",即一个让用户对 Labelmap 分割数据进行读取、编辑与导出的专用模式。
在 modes/segmentation/package.json 中,该模式包名为@ohif/mode-segmentation,版本号与平台主线保持一致,其description字段与 README 定义完全对应。值得注意的依赖结构:
- peerDependencies覆盖了模式运行所需的全部扩展:
@ohif/core、@ohif/extension-cornerstone、@ohif/extension-cornerstone-dicom-seg、@ohif/extension-cornerstone-dicom-rt、@ohif/extension-cornerstone-dicom-sr、@ohif/extension-default、@ohif/extension-dicom-pdf、@ohif/extension-dicom-video,以及基础模式@ohif/mode-basic; - 其中 SEG(分割)与 RT(放疗结构)扩展直接决定了该模式的分割能力边界,SR 扩展则提供测量/报告的显示支持;
- 包入口为
module: "src/index.tsx",即模式实例定义本身。
从实现看,Segmentation 模式不是从零搭建的,而是以@ohif/mode-basic为基座进行扩展:它复用了 Basic 模式的modeFactory、onModeEnter、onModeExit、isValidMode、layoutTemplate与extensionDependencies,再叠加自己特有的布局、工具组、工具栏与生命周期逻辑(见 modes/segmentation/src/index.tsx)。
模式实例:布局、路由与生命周期
模式核心定义
在 modes/segmentation/src/index.tsx 中,modeInstance是模式的“自描述”数据对象,包含以下关键字段:
export const modeInstance = { id, routeName: 'segmentation', displayName: 'Segmentation', toolbarButtons: [ { $reference: 'cornerstone.toolbarButtons' }, { $reference: 'cornerstone.segmentationToolbarButtons' }, ], toolbarSections: [ { $reference: 'cornerstone.segmentationModeToolbarSections' }, { $reference: 'cornerstone.segmentationToolbarSections' }, ], toolGroupAdditions: { default: [], mpr: [], volume3d: [] }, initToolGroups, modeCustomizations: 'segmentationModeCustomizations', activatePanelTriggers: [], onModeEnter, onModeExit, isValidMode, nonModeModalities: ['SM', 'ECG', 'OT', 'DOC'], routes: [segmentationRoute], extensions: extensionDependencies, hangingProtocol: ['@ohif/mnGrid'], sopClassHandlers: [ohif.sopClassHandler, segmentation.sopClassHandler, dicomRT.sopClassHandler], };要点解读:
- routeName为
segmentation,路由路径为template,挂载在segmentationRoute上; - toolbarButtons / toolbarSections使用
$reference引用语法,属于“组合式”声明:它们指向 cornerstone 扩展中注册的通用工具栏按钮与分割专用按钮、分区(见下文工具栏小节),而不是在模式内硬编码按钮列表; - nonModeModalities声明
['SM', 'ECG', 'OT', 'DOC']为“非模式模态”——当研究(Study)只包含这些模态时模式判定为无效,即无法对纯超声(SM)、心电(ECG)、其他(OT)、文档(DOC)影像进行分割; - hangingProtocol显式优先使用网格布局协议
@ohif/mnGrid,便于多视口同步分割; - sopClassHandlers只注册三个处理器:默认栈处理器(普通影像)、SEG 处理器(分割)与 RT 处理器(放疗结构),这正是该模式的分割职能在数据层的体现。
布局:左右面板与三种视口
segmentationLayout继承自 Basic 模式的布局 id(ohif.layout),但在右面板上做了关键替换:
export const segmentationLayout = { id: ohif.layout, props: { leftPanels: [ohif.thumbnailList], leftPanelResizable: true, rightPanels: [ cornerstone.labelMapSegmentationPanel, // Labelmap 分割面板(含编辑工具) cornerstone.contourSegmentationPanel, // Contour 分割面板(含编辑工具) ], rightPanelResizable: true, viewports: [ { namespace: cornerstone.viewport, displaySetsToDisplay: [ohif.sopClassHandler] }, { namespace: segmentation.viewport, displaySetsToDisplay: [segmentation.sopClassHandler] }, { namespace: dicomRT.viewport, displaySetsToDisplay: [dicomRT.sopClassHandler] }, ], }, };对照 modes/basic/src/index.tsx 中 Basic 模式的basicLayout,两者的差异一目了然:
| 维度 | Basic 模式 | Segmentation 模式 |
|---|---|---|
| 右面板 | cornerstone.segmentation+cornerstone.measurements(测量优先) | Labelmap 分割面板 + Contour 分割面板(分割优先) |
| 视口命名空间 | 6 类视口(含 SR、PDF、ECG 等) | 3 类视口:常规影像、SEG 分割、RT 结构 |
| 右面板默认状态 | rightPanelClosed: true | 无该字段(右面板展开) |
布局中注释明确指出:面板列表以“字面量”形式在模式路由进入时注入到标准的leftPanels/rightPanels定制项中,因此模式阶段的定制块与全局定制都可以在布局解析前覆盖它们——这是 OHIF 定制化体系(Customization)在该模式上的直接体现。
生命周期钩子:在 Basic 之上叠加自动切页
模式定义了自己的onModeEnter,其实现先调用 Basic 模式的onModeEnter(负责初始化工具组、注册工具栏、应用toolGroupAdditions、清理测量等),随后额外挂载分割面板自动切换处理器:
export function onModeEnter(ctx: withAppTypes) { basicOnModeEnter.call(this, ctx); const { segmentationService, viewportGridService, panelService } = ctx.servicesManager.services; const { unsubscribeAutoTabSwitchEvents } = setUpAutoTabSwitchHandler({ segmentationService, viewportGridService, panelService, }); this._unsubscriptions.push(...unsubscribeAutoTabSwitchEvents); }而onModeExit直接复用 Basic 模式的实现(见 modes/basic/src/index.tsx),它会统一执行this._unsubscriptions中的全部取消订阅函数,并销毁 toolGroupService、syncGroupService、segmentationService 与 cornerstoneViewportService——因此 Segmentation 模式在onModeEnter中 push 的订阅能被正确清理,不会泄漏。
分割面板自动切换:Labelmap 与 Contour 的智能联动
setUpAutoTabSwitchHandler(见 modes/segmentation/src/utils/setUpAutoTabSwitchHandler.ts)是该模式区别于 Basic 模式的核心增强逻辑。它解决的实际问题是:当用户首次添加分割时,右面板应自动切换到与分割类型匹配的标签页。
实现要点:
const autoTabSwitchEvents = [ segmentationService.EVENTS.SEGMENTATION_MODIFIED, segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, ]; let shouldSwitchTab = true; const unsubscribeAutoTabSwitchEvents = autoTabSwitchEvents .map(eventName => segmentationService.subscribe(eventName, () => { const segmentations = segmentationService.getSegmentations(); if (!segmentations.length) { shouldSwitchTab = true; // 全部分割被移除后,下次添加时重新触发切换 return; } const activeViewportId = viewportGridService.getActiveViewportId(); const activeRepresentation = segmentationService .getSegmentationRepresentations(activeViewportId) ?.find(representation => representation.active); if (activeRepresentation && shouldSwitchTab) { shouldSwitchTab = false; switch (activeRepresentation.type) { case 'Labelmap': panelService.activatePanel( '@ohif/extension-cornerstone.panelModule.panelSegmentationWithToolsLabelMap', true ); break; case 'Contour': panelService.activatePanel( '@ohif/extension-cornerstone.panelModule.panelSegmentationWithToolsContour', true ); break; } } }) ) .map(subscription => subscription.unsubscribe);逻辑拆解:
- 事件源:订阅
SEGMENTATION_MODIFIED与SEGMENTATION_REPRESENTATION_MODIFIED两个事件,覆盖分割数据被修改或分割表示(表示类型)被修改的场景; - 状态复位:当
getSegmentations()返回空数组(全部分割被删除)时,将shouldSwitchTab置回true,确保下一次新增分割时再次触发自动切换; - 按类型分派:通过
viewportGridService.getActiveViewportId()找到当前活动视口,再在其分割表示中查找active标记的表示,依据type字段在Labelmap与Contour之间分派到不同的面板 ID(panelSegmentationWithToolsLabelMap/panelSegmentationWithToolsContour); - 一次性语义:
shouldSwitchTab标志位保证整个会话中只在首次添加分割时切换一次,之后用户手动切换的标签页状态不被干扰。
返回值{ unsubscribeAutoTabSwitchEvents }是取消订阅函数数组,正好被onModeEnter通过this._unsubscriptions.push(...)纳入统一清理。
工具组初始化:default / mpr / volume3d 三套配置
Segmentation 模式的工具组初始化在 modes/segmentation/src/initToolGroups.ts,入口函数签名与 Basic 模式完全一致:
function initToolGroups({ extensionManager, toolGroupService, commandsManager }) { initDefaultToolGroup(extensionManager, toolGroupService, commandsManager, 'default'); initMPRToolGroup(extensionManager, toolGroupService, commandsManager); initVolume3DToolGroup(extensionManager, toolGroupService); }- default:2D 轴位工作流的主工具组;
- mpr:多平面重建(MPR)工具组,额外绑定 Crosshairs(十字线)并禁用 ReferenceLines;
- volume3d:3D 体渲染工具组,仅含旋转、缩放、平移三件套。
toolGroupAdditions字段({ default: [], mpr: [], volume3d: [] })为后续定制预留了空数组钩子——扩展模式或?customization=模块可以通过mode阶段的$push命令向这三个工具组追加额外工具。
活跃工具与鼠标绑定
三套工具组的活跃(active)工具保持一致,即分割模式下的基础导航能力:
const tools = { active: [ { toolName: toolNames.WindowLevel, bindings: [{ mouseButton: Enums.MouseBindings.Primary }] }, { toolName: toolNames.Pan, bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }] }, { toolName: toolNames.Zoom, bindings: [{ mouseButton: Enums.MouseBindings.Secondary }, { numTouchPoints: 2 }], }, { toolName: toolNames.StackScroll, bindings: [{ mouseButton: Enums.MouseBindings.Wheel }, { numTouchPoints: 3 }], }, ], ... };对应的按键/触控约定为:Primary=WindowLevel(窗宽窗位)、Auxiliary=Pan(平移)、Secondary=Zoom(缩放)、滚轮=StackScroll(翻页),双指/三指触摸分别映射缩放与翻页。volume3d 工具组则把 Primary 换成了TrackballRotateTool(旋转)。
被动工具:完整的分割编辑工具清单
Segmentation 模式在passive列表中注册了远超 Basic 模式的分割编辑工具矩阵,这是本模式与 Basic 模式在工具层面最显著的区别。全部工具在创建后经由commandsManager.run('initializeSegmentLabelTool', { tools })统一初始化分割标签工具。
按功能可归类如下:
画笔与擦除器(Brush / Eraser 家族)——均以Brush为父工具,通过activeStrategy指定策略,并统一受MIN_SEGMENTATION_DRAWING_RADIUS/MAX_SEGMENTATION_DRAWING_RADIUS约束(见 modes/segmentation/src/constants.ts:0.5与99.5):
| 工具名 | activeStrategy | 用途 |
|---|---|---|
CircularBrush | FILL_INSIDE_CIRCLE | 圆形画笔填充 |
CircularEraser | ERASE_INSIDE_CIRCLE | 圆形擦除 |
SphereBrush | FILL_INSIDE_SPHERE | 球状画笔(体数据) |
SphereEraser | ERASE_INSIDE_SPHERE | 球状擦除 |
ThresholdCircularBrush | THRESHOLD_INSIDE_CIRCLE | 阈值圆形画笔 |
ThresholdSphereBrush | THRESHOLD_INSIDE_SPHERE | 阈值球状画笔 |
ThresholdCircularBrushDynamic | THRESHOLD_INSIDE_CIRCLE+threshold: { isDynamic: true, dynamicRadius: 3 } | 动态阈值圆形画笔 |
ThresholdSphereBrushDynamic | THRESHOLD_INSIDE_SPHERE+threshold: { isDynamic: true, dynamicRadius: 3 } | 动态阈值球状画笔 |
剪刀工具(Scissors):CircleScissors(圆形)、RectangleScissors(矩形)、SphereScissors(球状),用于快速框选区域生成分割。
分割辅助工具:LabelmapSlicePropagation(切片传播)、MarkerLabelmap(标记)、ClickSegment(点击选中分割段)、SegmentBidirectional(分割内双向测量)、SegmentSelect(分割段选择)、LabelMapEditWithContourTool(用轮廓编辑标签图)。
轮廓分割(Contour Segmentation)工具:PlanarFreehandContourSegmentation(自由手绘轮廓)、LivewireContourSegmentation(活线/磁吸轮廓)、SculptorTool(雕刻工具)。
样条 ROI 工具——以SplineContourSegmentation为父工具,全部开启enableTwoPointPreview两点预览:
CatmullRomSplineROI:type: 'CATMULLROM';LinearSplineROI:type: 'LINEAR';BSplineROI:type: 'BSPLINE'。
测量与浏览工具:PlanarFreehandROI、WindowLevelRegion、Magnify、StackScroll、UltrasoundDirectional。
禁用工具:ReferenceLines与AdvancedMagnify在 default 组中被显式禁用(disabled列表)。
MPR 工具组的 Crosshairs 细节
MPR 工具组除复用上述工具外,还以特殊方式注册Crosshairs(见 modes/segmentation/src/initToolGroups.ts 中initMPRToolGroup):
- 将 Crosshairs 绑定到Primary + Shift(
mouseButton: Primary, modifierKey: Shift),使其独占一个鼠标组合键——注释解释了原因:若不绑定独立按键,Crosshairs 会在 Primary 上激活,而它是disableOnPassive的,一旦工具栏激活其他 Primary 工具(画笔/缩放/平移)就会被立即禁用,从而与它们互斥;绑定独立按键后可与之共存; - 开启
viewportIndicators(视口指示器),配置circleRadius: 5、xOffset: 0.95、yOffset: 0.05; - 通过
getReferenceLineColor根据视口 id 或方位返回参考线颜色:viewport-0为rgb(200, 0, 0)、viewport-1为rgb(200, 200, 0)、viewport-2为rgb(0, 200, 0),按方位(axial/sagittal/coronal)同样有对应色,兜底色为#0c0; autoPan默认关闭(enabled: false, panSize: 10)。
MPR 工具组同时把ReferenceLines加入disabled,避免与 Crosshairs 的参考线能力冲突。
工具栏定制:分割按钮包与分区
模式实例通过$reference引用了 cornerstone 扩展注册的三个定制项,其定义位于 extensions/cornerstone/src/customizations/segmentationToolbarCustomization.ts:
cornerstone.segmentationToolbarButtons:分割编辑工具栏按钮包,与通用按钮包cornerstone.toolbarButtons一起构成该模式的默认按钮集合;cornerstone.segmentationModeToolbarSections与cornerstone.segmentationToolbarSections:工具栏分区容器定义。
在按钮包内部,结构非常清晰(以BrushTools、LabelMapUtilities、ContourUtilities、LabelMapTools、ContourTools为容器):
BrushTools/LabelMapTools/ContourTools使用ohif.toolBoxButtonGroup(嵌套工具盒子按钮组);LabelMapUtilities/ContourUtilities使用ohif.Toolbar(内嵌工具条);- 具体工具按钮(如
PlanarFreehandContourSegmentationTool)配置了evaluate评估函数(如evaluate.cornerstone.segmentation在无分割时显示"Create new segmentation to enable this tool."禁用提示、evaluate.cornerstone.hasSegmentationOfType校验 Contour 类型分割是否存在)以及commands命令序列(激活工具、激活对应类型的分割表示等)。
值得注意的是,该定制文件也导出了与模式常量一致的MIN_SEGMENTATION_DRAWING_RADIUS = 0.5/MAX_SEGMENTATION_DRAWING_RADIUS = 99.5,印证画笔半径上下限是跨模式与工具栏统一约定的。工具栏注释还提示:这些按钮包可被segmentationEditing.jsonc等定制拉入 Basic / Longitudinal 模式,即分割编辑能力可以被其他模式按需复用。
模式有效性校验与自定义定制块
数据驱动的有效性判断
Segmentation 模式复用了 Basic 模式导出的isValidMode(实现见 modes/basic/src/index.tsx),其判定逻辑为数据驱动:
- 若存在
excludedStudies:研究中每条属性均命中任一排除项则无效; - 若存在
excludedModalities:研究包含其中任一模态则无效; - 若存在
modeModalities:研究至少包含一个列出的模态才有效(数组条目要求全部模态都存在); - 否则依据
nonModeModalities:只要研究包含任意一个不在该列表中的模态即有效。
Segmentation 模式设置nonModeModalities: ['SM', 'ECG', 'OT', 'DOC'],因此:只有当研究完全由这些模态构成时,模式才不可用;只要存在任何可分割的影像模态(如 CT、MR、PT),模式即为有效。
定制块:可编辑的分割面板
模式注册了segmentationModeCustomizations定制块(见 modes/segmentation/src/index.tsx):
export const customizations = { segmentationModeCustomizations: {}, };注释明确说明:与 Basic 模式注册的basicModeCustomizations中'panelSegmentation.disableEditing': true(Basic 模式默认禁用分割面板编辑)不同,Segmentation 模式的定制块是空的——分割面板在该模式下是可编辑的。该块虽为空,但仍被注册,以便 bootstrap 或?customization=模块以模式作用域追加值。这正是该模式“默认可编辑”语义的定制化实现。
基于 modeFactory 派生自定义分割模式
模式的最终导出对象包含modeFactory(来自 Basic 模式):
const mode = { id, modeFactory, modeInstance, extensionDependencies, customizations, };modeFactory使用immutability-helper的update将modeConfiguration应用到modeInstance上(见 modes/basic/src/index.tsx 中modeFactory实现)。这意味着站点可以定义一个mySegmentation模式,通过 modeConfiguration 以不可变更新的方式扩展本模式,例如:
- 替换
initToolGroups为自定义工具组初始化函数(initToolGroups已被模式显式导出:export { initToolGroups }); - 通过
$push向toolbarButtons/toolGroupAdditions追加按钮与工具; - 覆盖
modeCustomizations以调整面板行为; - 修改
nonModeModalities、hangingProtocol或sopClassHandlers以适配特定临床工作流。
与相关扩展的协同
Segmentation 模式的运转依赖若干扩展的能力注入(均在 modes/basic/src/index.tsx 的extensionDependencies中声明):
@ohif/extension-cornerstone-dicom-seg:提供 SEG 的 SOP Class Handler 与专用视口(见 extensions/cornerstone-dicom-seg/src/index.tsx,视口以React.lazy懒加载OHIFCornerstoneSEGViewport),承担分割数据的解析、显示与渲染;@ohif/extension-cornerstone-dicom-rt:提供 RTSTRUCT 的读取与渲染,使放疗结构可与分割同屏对照;@ohif/extension-cornerstone:提供全部影像交互工具(utilityModule.tools)、分割面板(Labelmap/Contour)与分割工具栏按钮包;@ohif/mode-basic:提供模式工厂、生命周期基座与有效性校验逻辑。
小结
Segmentation 模式以极简的包描述(README 仅一句话:启用 Labelmap 分割的读/编辑/导出)承载了一套完整的分割工作流实现:它复用 Basic 模式的生命周期与组合机制,替换为双分割面板布局与三视口路由,注册了覆盖画笔、擦除器、剪刀、阈值、动态阈值、样条与轮廓分割的完整工具矩阵,并通过setUpAutoTabSwitchHandler在 Labelmap 与 Contour 表示之间智能切换面板。对于需要搭建专用分割应用的开发者,该模式既可直接使用,也可通过modeFactory与定制化体系低成本地派生定制版本——其完整实现均可从 modes/segmentation/src/index.tsx、modes/segmentation/src/initToolGroups.ts 与 extensions/cornerstone/src/customizations/segmentationToolbarCustomization.ts 中继续深入研读。
【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考