前言

由于工作接触影像项目,用到了cornerstone及其相关的库,网上感觉资料也挺少,于是用到啥记录啥吧。开始第一篇,cornerstoneTools的Synchronizer相关。

在Example页面中( https://tools.cornerstonejs.org/examples )可以看到有Synchronization这一列,就是同步的例子,比如扫描定位线、不同的图像同步修改调窗等。总之,运用的场景就是需要状态联动的地方。

关于Synchronizer

cornerstoneTools提供了一个同步化的功能,文档里找到相关内容,可以从Synchronizer源码看它的使用方法。

首先export一个Synchronizer的类,参数是event和handler。event是注册的事件名(可以是多个用空格分开),当事件触发时,该synchronization就会触发。handler是synchronization触发时,target element要执行的方法。

sourceElements和targetElements是Synchronizer的两个重要属性,分别存放了同步的源和目标对象,上面说到event触发,即是触发了源对象(sourceElements)上注册的事件,触发后目标对象(targetElements)会执行handler。

部分实例上的方法 :

addSource :参数为element,添加到sourceElements,给该element注册事件(Synchronizer的event)

addTarget :参数为element,添加到targetElements

add :参数为element,调用了addSource和addTarget

(remove同理

getSourceElements :获取源对象集合

getTargetElements :获取目标对象集合

setViewport :调用了cornerstone的setViewport

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Synchronize target and source elements when an event fires on the source element
* @export @public constructor
* @name Synchronizer
*
* @param {String} event - The event(s) that will trigger synchronization. Separate multiple events by a space
* @param {Function} handler - The function that will make the necessary changes to the target element in order to synchronize it with the source element
*/
function Synchronizer(event, handler) {
...
const sourceElements = [];
const targetElements = [];
...
this.addSource = function(element){...}
this.addTarget = function(element){...}
this.add = function(element){...}
this.remove = function(element){...}
...
}

实际场景

我这边的场景是,要实现多个影像序列viewport的相对同步(如序列1调窗windowWidth和windowCenter各增加了100,那同步的序列2也要这两个属性在原本的基础上各加100酱紫,有点废话- -),那先想下大概要做的事:

  • 自定义一个相对同步事件
  • 实例化Synchronizer,添加好source和target
  • 新建一个handler,用来处理target对象(也是Synchronizer的参数)
  • 在操作的时候触发自定义事件
1.自定义事件
1
2
3
4
// cornerstone-core-plus.js
export const EVENTS = {
RELATIVE_SYNC: 'cornerstonerelativesync'
}

从设想中可以感觉到,要统一控制所有的改变,肯定要对原本cornerstone的setViewport做手脚,所以这个文件就写一些对cornerstone-core使用的补充。

2.Synchronizer实例化
1
2
3
4
5
6
7
export const linkSynchronizer = new cornerstoneTools.Synchronizer(
EVENTS.RELATIVE_SYNC,
cornerstoneTools.linkSynchronizer
);
// 某view层面
linkSynchronizer.add(element);

第一个参数就是上面定义的事件,第二个参数是handler,由于我这边在cornerstoneTools上做了很多扩展,所以直接加到cornerstoneTools上了。在开启同步的时候调用add方法,把elemen同时t加到source和target上(因为是同步功能,所以随便哪个都可以当源)

3.新建handler

从Synchronizer源码中可以看到handler在调用时传入的参数,实例本身、触发的源对象、当前目标对象、事件传过来的详情内容。这边主要要靠 eventData 来传递操作详情。

可见eventData 的源头是 onEvent 方法中的参数,即 addSource 时注册的事件。cornerstone这儿事件的触发调用 cornerstone.triggerEvent(el, type, detail) 方法,detail就是最终传过来的e.detail,这样就明了了,只要trigger的时候带上需要的数据即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// cornerstoneTools源码 - Synchronize.js片段
eventHandler(
that,
sourceElement,
targetElement,
eventData,
positionDifference
)
...
element.addEventListener(oneEvent, onEvent);
...
function onEvent(e) {
const eventData = e.detail;
if (ignoreFiredEvents === true) {
return;
}
fireEvent(e.currentTarget, eventData);
}

下面就开始写handler,需求是所有的变化都是相对变化,所以在项目中其实有几种情况:

1.比如鼠标左键的拖动改变调窗、缩放、移动这种操作,需要计算相对位移,所以监听鼠标事件,记录mousedown时的viewport,作为 originViewporteventData 中传过来

2.比如固定窗高窗位的设置,就不需要 originViewport,只要直接把 targetElement 的对应属性设置成sourceElement viewport的属性值就行了

3.比如顺时针旋转这种操作,需要知道旋转的度数,所以传入 changeData

activeTool 是为了区别当前做的是什么操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 即cornerstoneTools.linkSynchronizer
import cornerstoneTools from 'cornerstone-tools';
const { external } = cornerstoneTools;
export default function (synchronizer, sourceElement, targetElement, eventData) {
// 防止死循环
if (targetElement === sourceElement) {
return;
}
const { originViewport, activeTool, changeData } = eventData;
if (!activeTool) {
return
}
const cornerstone = external.cornerstone;
const sourceViewport = cornerstone.getViewport(sourceElement);
const targetViewport = cornerstone.getViewport(targetElement);
// 拿缩放举个例子
switch (activeTool) {
case 'Zoom':
return handleZoom();
...
}
function handleZoom() {
const scaleTarget = targetViewport.scale;
const scaleSource = sourceViewport.scale;
if (originViewport) {
const scaleOrigin = originViewport.scale;
if (scaleSource === scaleOrigin) {
return
}
targetViewport.scale = scaleTarget + (scaleSource - scaleOrigin);
} else {
if (scaleTarget === scaleSource) {
return
}
targetViewport.scale = scaleSource;
}
synchronizer.setViewport(targetElement, targetViewport);
}
}
4.事件的触发

上面也提到了,就是在操作的时候调用 cornerstone.triggerEvent(el, type, detail) 方法,按设计调用的地方有两个。

1.鼠标操作处(3中的情况1),当mouseup的时候触发,传过去当前的tool和mousedown时存下的originViewport。

2.全部setViewport的地方(准确说是所有使得当前的source的图像改变的地方),由于原本修改viewport的地方都是调用的setViewport,所以要对这个方法扩展一下,重新定义一个 setViewportWithEvent 方法代替原本调用setViewport的地方。

1
2
3
4
5
6
7
export function setViewportWithEvent(element, viewport, activeTool, changeData) {
cornerstone.setViewport(element, viewport);
cornerstone.triggerEvent(element, EVENTS.RELATIVE_SYNC, {
activeTool,
changeData
})
}

最后

同步化这块儿把大体设计了解了后就比较容易了,的确对很多功能的实现很有帮助,比如上文的联动,还有扫描定位线、序列图像同步滚动等等场景。还有些没用过的属性、方法,用到的时候只能多读读源码了- -