ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

OHIF Display Set 排序机制深入解析:sortStudy、getLatestInstanceDateTime 与保存时间戳

2026/9/18 16:27:35 拓冰建站 浏览量
OHIF Display Set 排序机制深入解析:sortStudy、getLatestInstanceDateTime 与保存时间戳 OHIF Display Set 排序机制深入解析sortStudy、getLatestInstanceDateTime 与保存时间戳【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers导读本文以 OHIF 官方开发文档《Notes and Requirements for general OHIF behaviour》中关于 Series 与 Display Set 排序的规范为核心结合platform/core的实际源码与测试完整讲解 OHIF 是如何对系列、显示集和实例进行排序的包括addSameSeriesCompare同系列比较器注册机制、sortVector系列拆分排序向量、getLatestInstanceDateTime的日期时间选取规则、updateNewInstanceMetadata的保存打戳逻辑以及它们背后隐藏的排序一致性问题。读完本文你将理解 OHIF 中「报告、分割、结构集排在图像之后并按创建时间倒序」这一行为的全部实现细节并能在开发自定义 SOP Class Handler 或拆分系列时写出正确、稳定的排序代码。为什么需要显式的排序规则用户经常希望看到排好序的系列列表更一般地说是排好序的Display Set显示集列表。Series 是原始数据可以被拆分成多个 Display Set但二者本质上属于同一类排序对象。文档给出了两个典型场景一个 MR 系列可能同时包含 T1 和 T2 回波用户希望 T2 排在 T1 之后一个系列可能包含 4 个乳腺摄影视图LCC、RCC、LMLO、RMLO用户希望所有CC视图排在前面而在同一CC子类型内部左侧视图又排在右侧之前。这类同一系列内部的排序需求无法靠单一的全局比较函数满足因此 OHIF 设计了按名称注册同系列比较器的机制即sortStudy.ts中的addSameSeriesCompare。同系列比较器addSameSeriesCompare注册机制在 platform/core/src/utils/sortStudy.ts 中比较器存放在一个Mapstring, CompareSameSeries中CompareSameSeries包含priority优先级数值和compare比较函数两个字段type CompareSameSeries { priority: number; compare: (a, b) number; }; const mapCompareSameSeries new Mapstring, CompareSameSeries(); export function addSameSeriesCompare(name: string, compareF: (a, b) number, priority: number) { if (!compareF) { mapCompareSameSeries.delete(name); } else { mapCompareSameSeries.set(name, { compare: compareF, priority }); } }注意两点删除注册传入null作为比较函数即可删除注册mapCompareSameSeries.delete(name)。在迁移指南 display-set-ordering.md 中明确提到若你希望恢复 3.13 的旧行为仅按实例号排序可以这样取消注册addSameSeriesCompare(name, null, priority);priority 参数每个比较器附带一个默认优先级用于两个 Display Set 注册了不同比较器名时的跨比较器排序。文档中的注册示例addSameSeriesCompare(mammographyCompare, mammographyCompare, 5) addSameSeriesCompare(mrT1T2Compare, mrT1T2Compare, 7);然后对应的 Display Set 需要通过compareSameSeries字段声明它使用哪个比较器displaySet { ..., compareSameSeries: mammographyCompare, }compareSameSeries字段在 platform/core/src/types/DisplaySet.ts 中有正式类型定义compareSameSeries?: string其含义是当比较来自同一 seriesInstanceUID 的显示集时使用的比较函数名称。比较器如何生效核心函数是 compareSameSeriesDisplaySetexport const compareSameSeriesDisplaySet (a, b) { const { compareSameSeries: compareAName default } a; const { compareSameSeries: compareBName default } b; const compareA mapCompareSameSeries.get(compareAName); const compareB mapCompareSameSeries.get(compareBName); if (compareA compareB) { const compareValue compareA compareB ? compareA.compare(a, b) : compare(compareA.priority, compareB.priority); if (compareValue) { return compareValue; } } return sortByInstanceNumber(a.instance, b.instance); };逻辑分三层两侧 Display Set 的compareSameSeries名称相同 → 使用注册在该名称下的比较函数名称不同 → 使用两个比较器的priority值大小priority 小者在前以上比较结果为 0平局或未注册 → 回退到按实例号比较sortByInstanceNumber。在 3.14 版本之前compareSameSeriesDisplaySet存在一个 bug只有比较器返回0时才采用其结果真正分出先后时反而丢弃并回退到实例号比较导致注册的比较器完全不生效。3.14 修复后按文档语义生效详见迁移指南 addSameSeriesCompare comparators now run。系列排序的完整比较链compareSeriesUID将 UID 比较与同系列比较器串联起来sortStudy.ts#L64-L65export const compareSeriesUID (a, b) compare(a.SeriesInstanceUID, b.SeriesInstanceUID) || compareSameSeriesDisplaySet(a, b);也就是说先按 SeriesInstanceUID 分组UID 相同的 Display Set 才进入同系列比较器。这是整个排序体系的入口。从系列拆分指定排序顺序sortVectorgetSopClassHandlerModule系列拆分规则可以在创建 Display Set 时通过添加一个sortVector字段来指定同一系列内 Display Set 的自定义顺序。规则如下具有相同 SeriesInstanceUID 的 Display Set 之间使用 sortVector 进行比较向量的第一个元素是该类型值在所有排序类型中的总体排序优先级必须是数值向量中剩余的值对所有第一个值相同的 sortVector 必须保持一致即向量长度与后续各位置的语义要统一。文档给出的乳腺摄影示例// LCC view [25, CC, L, XO] // BCC view [24, CC, B]这里主排序值25表示该系列的总体排序位置随后三个值分别表示 view type、sub type 和 side。而双侧both视图希望排在所有视图之前因此其主值被赋为小于25的数值如24。由于compareSameSeriesDisplaySet在两 Display Set 注册比较器不同时会先比较 prioritysortVector本质上提供了一种数据驱动的、无需注册 JS 比较函数的同类排序途径。注sortVector目前仅在本文档中给出规格说明见 notes-requirements.md使用它时需要保证同一系列内所有 Display Set 的首元素数值一致可比、剩余元素语义一致。Display Set 的日期与时间getLatestInstanceDateTime为什么需要显示集自己的日期时间派生系列报告 SR、分割 SEG、结构集 RTSTRUCT在系列列表中排在图像之后并按日期/时间倒序排列使最新创建的派生对象离图像最近。要做到这一点每个 Display Set 需要一个统一的创建时间。关键问题在于Display Set 是从其系列中的某一个实例创建的而这个实例本身携带的是系列的SeriesDate/SeriesTime。例如向一个已存在的 SR 系列中再保存一份报告这份新报告的SeriesDate/SeriesTime仍然是该系列首次创建时的日期——只有实例级的日期/时间才能说明这份报告是刚刚生成的。因此排序必须依据实例级创建时间而不是系列级日期。日期时间属性的选取规则getLatestInstanceDateTime的实现位于 platform/core/src/utils/latestInstanceDateTime.ts。DICOM 用多组属性对记录何时创建具体出现哪一组取决于模态和写入方因此该函数从以下候选属性中选一组dateTimeAttributes属性对说明InstanceCreationDate/InstanceCreationTime实例级SOP Common 模块提供ContentDate/ContentTime实例级AcquisitionDate/AcquisitionTime实例级AcquisitionDateTime组合 DT 值增强型多帧对象常用StructureSetDate/StructureSetTimeRTSTRUCT 专属PresentationCreationDate/PresentationCreationTimePR 专属SeriesDate/SeriesTime系列级选取规则latestInstanceDateTime.ts#L238-L255 中的consider逻辑日期取所有这些属性中最晚的日期时间取携带该完全相同日期的属性中最晚的时间——时间永远不会与一个不是它一起到达的日期组合因此结果总是真实发生过的一个日期/时间当胜出日期没有任何属性携带时间时时间为空排序精度只到天——这是数据允许的最好结果StudyDate/StudyTime不参与同一 Study 的每个系列都共享它们无法区分系列非 DICOM DA 格式的值视为无日期。例如某些系列级元数据携带已格式化用于显示的日期19-Jan-2026若被当成数字会读成192026按月中某日排序因此必须拒绝带 UTC 偏移ZZXX的AcquisitionDateTime会通过expandDicomDateTime被移动到查看器的时区读出的日期/时间是同一时刻的本地挂钟读数。getLatestInstanceDateTime还接受数组输入——多实例派生系列的日期/时间取其最新创建的实例的日期/时间latestInstanceDateTime.ts#L231-L232。UTC 偏移的处理expandDicomDateTimeAcquisitionDateTime是 DICOM DT 值可以YYYYMMDDHHMMSS.ffffffZZXX形式携带 UTC 偏移。expandDicomDateTimelatestInstanceDateTime.ts#L104-L159负责将其换算到查看器本地时区声明了偏移的 DT → 移动到查看器时区返回本地挂钟读数。例如-0400的正午是 UTC 16:00而 UTC 16:00 在-0600时区是 10:00未声明偏移的 DT → 原样读取无从得知其所属时区与其它裸 DA/TM 的处理一致只含日期的 DT → 表示该日的开始移动后必然带回时间即使它声明的是查看器自身偏移也带回时间——两个命名同一时刻的 DT 值必须给出同一答案否则纯日期会排在带时间的等价时刻之前查看器在该时刻的偏移被使用因此跨季节夏令时采集也能正确换算。对应的测试见 latestInstanceDateTime.test.js其中验证了20260819100000.000000-0500与20260819150000.0000000000在任意查看器时区下都得到相同的排序键。排序键的生成getDateTimeSortKey 与 getLatestInstanceDateTimeSortKey为了可比较日期和时间会转为定宽字符串键latestInstanceDateTime.ts#L281-L296getDateTimeSortKey(date, time)无日期返回排在最旧无时间则排在同日期所有带时间值之前时间被补齐到定宽因为HHMM与HHMMSS命名同一时刻却无法直接比较getLatestInstanceDateTimeSortKey(source)即先取getLatestInstanceDateTime再转排序键。排序键必须从 Display Set 读取而非 displaySet.instancecompareSeriesDateTime与dateTimeSortKey读取的是Display Set 自身的seriesDate/SeriesTime绝不去读displaySet.instancesortStudy.ts#L67-L100。原因有二键不一致会掩盖同系列比较compareSameSeriesDisplaySet只在键平局时才运行。getLatestInstanceDateTime(instance)在一个被拆分的系列的不同 Display Set 之间是不同的每个 Display Set 展示不同实例于是它们会按各自展示的实例排序注册的同系列比较器永远不会运行比较器不一致会产生循环若系列内用一个键、系列间用另一个键设系列 A 的两个 Display Set 夹住另一系列的 Display Set B会得到 A1 B、B A2 且 A2 A1 的循环Array.prototype.sort对同一输入返回不同结果。由此产生两个有意为之的推论在DisplaySet.ts的类型注释 platform/core/src/types/DisplaySet.ts#L66-L103 中有完整合同图像 Display Set直接取实例的SeriesDate/SeriesTime一个系列的所有实例携带完全相同的值因此同一系列的所有 Display Set 都持有相同值、键平局、在系列列表中保持相邻——这正是 extensions/default/src/getSopClassHandlerModule.js#L102-L103 中图像 Handler 的做法SeriesDate: instance.SeriesDate。图像 Display Set 的 Handler 绝不能调用getLatestInstanceDateTime因为它还会读取每个实例各不相同的AcquisitionDate/AcquisitionTime导致同一系列不同 Display Set 键不一致、列表顺序错误派生 Display SetSEG、RTSTRUCT、SR、PMAP、PDF、视频、图表取实例的创建日期/时间向已存在的 SEG 系列再保存一个分割时DisplaySetService会给新实例分配自己的 Display SetSEG/RTSTRUCT/PMAP Handler 没有addInstances该 Display Set 必须占据保存时刻的位置。而需要保持拆分的 Display Set 相邻时应给它们同一个值系列的值并注册addSameSeriesCompare比较器来排列彼此。另外若某个 Handler 的addInstances会推进 Display Set 展示的实例SR Handler 与 chart Handler 会追加到已有 Display Set 而非新建则必须重写这两个字段否则显示的日期仍是它所替换的报告的日期与系列列表刚排好的位置相矛盾。实例排序sortByInstanceNumber实例instance层面的排序在 sortByInstanceNumber 中默认按实例号递增排序仅当实例号无法裁决平局或双方都无实例号时才回退到下面的创建日期/时间最后再回退到 SOP Instance UID系列的最后一个实例被视为最近创建的——因此实例号无法指出谁最新时必须用创建时间替代同一实例的多帧frame共享该实例的所有日期/时间因此只按 frameNumber 排序这同时也让大体积多帧系列排序保持廉价所有帧共享同一实例号每对帧都会走到这一步若先排除 SOPInstanceUID 相同的对就能提前返回无 SOP Instance UID 的源如 Display Set 视图模型日期已格式化用于显示而不可比较保持原样。3.14 之前实例号之后直接回退到 SOP Instance UID现在改为先回退到创建日期/时间见迁移指南 Instances tie-break by creation date/time。保存时打戳updateNewInstanceMetadata为了让上述排序对新保存的对象也成立updateNewInstanceMetadataplatform/core/src/utils/updateNewInstanceMetadata.ts在 OHIF 保存每个报告、分割和结构集时打上当前日期/时间InstanceCreationDate/InstanceCreationTimeSOP Common 模块保证每个 IOD 都有该模态 IOD 定义的创建日期/时间对比系列中所有既有实例都高 1 的实例号。模态专属的创建属性updateNewInstanceMetadata.ts中的 modalityDateTimeAttributes 决定了写哪一对Modality属性对定义模块RTSTRUCTStructureSetDate/StructureSetTimeStructure Set 模块PRPresentationCreationDate/PresentationCreationTimePresentation State Identification 模块其它所有模态ContentDate/ContentTimeMulti-frame Functional GroupsSEG、SR Document GeneralSR、General Image图像系列关键约束RTSTRUCT 与 PR 的 IOD 不定义 ContentDate/ContentTime若给它们写上该属性严格校验器或归档系统可能拒绝该实例。而getLatestInstanceDateTime会读取全部三类属性对因此无论模态拿到哪一对排序结果一致。实例号必须基于全系列最高值实例号被设为系列内所有既有实例的最高实例号 1updateNewInstanceMetadata.ts#L107-L111。不能只从单一前驱实例推导系列中最近创建的实例不一定是实例号最大的那个若从单个前驱推导1 其编号可能与已存在实例冲突。系列级日期时间由 store 命令生成实例级打戳只覆盖对象加入既有系列时会移动的属性。而系列被创建时的日期/时间必须随对象一起生成updateNewInstanceMetadata无法区分新系列与既有系列两种情况由getCurrentDicomDateTimeupdateNewInstanceMetadata.ts#L16-L32计算并通过 store 命令传入。原因是 dcmjs 与适配器默认以UTC打上SeriesDate/SeriesTime和StructureSetDate/StructureSetTime——在其它时区这是错误的挂钟读数午夜附近还会差一天。生成时使用对象自身的时区有TimezoneOffsetFromUTC用之否则用本地时区这样 OHIF 保存的对象在全链路读取时都表现为同一个挂钟时刻。排序入口与默认行为sortStudy.ts对外暴露了从顶层到细节的完整排序入口sortStudy按排序标准默认 series 按 SeriesNumber 升序、instance 按 InstanceNumber 升序对 study 的 series 与 instances 就地排序deepSort控制是否深入排序实例sortStudySeries排序 series/Display Set 列表可注入自定义seriesSortingCriteriasortStudyInstances排序实例列表seriesSortCriteriadefault即seriesInfoSortingCriteria低优先级模态——如 SR/SEG 等——移到列表末尾且其中最新的排最前因为它通常最受关注其余按默认系列排序见 seriesInfoSortingCriteriasortDisplaySetsCopy返回排序后的 Display Set副本不修改输入支持studyInstanceUIDFirst选项把指定 Study 的 Display Set 排到最前、其余保持加载顺序sortImagesByPatientPosition当图像具备一致的ImagePositionPatient与ImageOrientationPatient时按扫描轴法向的投影距离做患者位置排序。seriesSortCriteria对象还被作为可替换策略使用应用层可通过自定义seriesSortingCriteria覆盖默认排序见SortDisplaySetsCopyOptions的注释 sortStudy.ts#L195-L203。与 3.13 → 3.14 迁移的衔接上述日期/时间排序规则与保存打戳行为自 3.14 起生效迁移指南 display-set-ordering.md 汇总了行为变化派生 Display Set 若其实例级日期/时间与系列日期/时间不同位置会改变系列本身不受影响系列排序始终是朴素的系列日期/时间排序如果你编写 SOP Class Handler两个字段都要写且addInstances推进实例时要重写什么都不写则回退到系列日期/时间即 3.13 行为如果你拆分系列为多个 Display Set给它们同一个值系列的值否则排序键不一致会掩盖addSameSeriesCompare比较器并产生循环存储对象新增了此前可能没有的属性若你曾依赖实例号取自单一前驱实例请注意现在基于全系列最高实例号推导导出名调整utils.getSeriesDateTime→utils.getLatestInstanceDateTimeutils.getSeriesDateTimeSortKey→utils.getLatestInstanceDateTimeSortKey类型SeriesDateTime→LatestInstanceDateTime模块seriesDateTime→latestInstanceDateTime。两个字段名SeriesDate/SeriesTime保持不变因为 Handler 需要把这两个字段赋给 Display Set。小结OHIF 的显示集排序是一套分层、可配置且对 DICOM 语义敏感的体系顶层按SeriesInstanceUID分组同组内由addSameSeriesCompare注册的比较器或sortVector决定次序平局回退实例号日期/时间键一律从 Display Set 自身读取图像与派生 Display Set 分别使用系列日期与getLatestInstanceDateTime从而保证同系列 Display Set 键平局、比较器能运行且比较器自洽无循环保存侧由updateNewInstanceMetadata打上实例级创建时间与全系列最高实例号 1由getCurrentDicomDateTime保证系列级日期时间使用正确的挂钟时区。无论是为自定义模态编写 SOP Class Handler、拆分系列还是处理多时区增强型数据这套规则都提供了确定性的排序结果值得作为 OHIF 扩展开发的必读规范。【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考