xingbofeng / xingbofeng.github.io

counterxing的博客
https://xingbofeng.github.io
175 stars 18 forks source link

cornerstone源码解析(一)——imageLoader #25

Open xingbofeng opened 6 years ago

xingbofeng commented 6 years ago

用了这么久的cornerstone,恰好这又是我的毕设题目,而且个人觉得cornerstone的代码写的算是比较优秀,所以打算从阅读源码的过程中多学习一点东西。

cornerstone加载医学影像显示的全过程

在cornerstone,一个enabled element就是一个HTML DOM节点(例如,一个div),cornerstone会在其中展示一些交互式的医学影像。为了展示这些影像,开发者需要做以下事情:

  1. 将cornerstone库通过script标签引入你的web页面中。
  2. 将cornerstone的图片加载器(image loaders)引入你的web页面中,图片加载器会使用WADOWADO-RScustom协议加载医学影像图片并提供给web页面使用。
  3. enabled element这个DOM节点加入到你的页面中去,在这个DOM节点内会展示医学影像。
  4. 使用CSS为这个element固定好宽度和高度。(切记一定要固定宽高)
  5. 调用enable(),准备在节点内展示影像。
  6. 调用loadImage()加载影像。
  7. 调用displayImage()展示影像。

看imageLoader

imageLoader一共有下列几个方法:

registerImageLoader(scheme, imageLoader)

对于cornerstonedicom影像的加载协议一般有以下几种:

这个方法就是用于注册dicom加载的方法,传入两个参数,一个是协议名称scheme,另一个则是加载方法imageLoader,这个方法可以自行注册,也可以使用cornerstoneWADOImageLoader或者cornerstoneWebImageLoader

示例:官方写的一个imageLoader

function loadImage(imageId) {
    // create a deferred object
    var deferred = $.Deferred();

    // Make the request for the DICOM data
    var oReq = new XMLHttpRequest();
    oReq.open("get", imageId, true);
    oReq.responseType = "arraybuffer";
    oReq.onreadystatechange = function(oEvent) {
        if (oReq.readyState === 4)
        {
            if (oReq.status == 200) {
                // request succeeded, create an image object and resolve the deferred
                // Code to parse the response and return an image object omitted.....
                var image = createImageObject(oReq.response);
                // return the image object by resolving the deferred
                deferred.resolve(image);
            }
            else {
                // an error occurred, return an object describing the error by rejecting
                // the deferred
                deferred.reject({error: oReq.statusText});
            }
        }
    };
    oReq.send();

    // return the pending deferred object to cornerstone so it can setup callbacks to be 
    // invoked asynchronously for the success/resolve and failure/reject scenarios.
    return deferred;
}

cornerstone首先声明一个imageLoaders字典,每次注册加载方法则往字典里写入这个加载方法

const imageLoaders = {};

// ...

/**
 * Registers an imageLoader plugin with cornerstone for the specified scheme
 *
 * @param {String} scheme The scheme to use for this image loader (e.g. 'dicomweb', 'wadouri', 'http')
 * @param {Function} imageLoader A Cornerstone Image Loader function
 * @returns {void}
 */
export function registerImageLoader (scheme, imageLoader) {
  imageLoaders[scheme] = imageLoader;
}

/**
 * Registers a new unknownImageLoader and returns the previous one
 *
 * @param {Function} imageLoader A Cornerstone Image Loader
 *
 * @returns {Function|Undefined} The previous Unknown Image Loader
 */
export function registerUnknownImageLoader (imageLoader) {
  const oldImageLoader = unknownImageLoader;

  unknownImageLoader = imageLoader;

  return oldImageLoader;
}

对于registerUnknownImageLoader(imageLoader),它指的是如果cornerstone没有识别到底使用的是什么协议加载dicom图片的时候,则使用这个方法进行图片加载。

loadImage(imageId, options)loadAndCacheImage(imageId, options)

loadImage(imageId, options)loadAndCacheImage(imageId, options)的差别在于是否缓存图片image obj,对于loadAndCacheImage(imageId, options),则会执行下列代码,把加载过的图片信息存入缓存:

putImageLoadObject(imageId, imageLoadObject);

至于如何存缓存,则深入putImageLoadObject(imageId, imageLoadObject)内部:

export function putImageLoadObject (imageId, imageLoadObject) {
  // 是否有imageId
  if (imageId === undefined) {
    throw new Error('putImageLoadObject: imageId must not be undefined');
  }
  // imageLoadObject是否合法
  if (imageLoadObject.promise === undefined) {
    throw new Error('putImageLoadObject: imageLoadObject.promise must not be undefined');
  }
  // imageLoadObject是否已经在缓存中
  if (imageCacheDict.hasOwnProperty(imageId) === true) {
    throw new Error('putImageLoadObject: imageId already in cache');
  }
  // 是否可被取消缓存
  if (imageLoadObject.cancelFn && typeof imageLoadObject.cancelFn !== 'function') {
    throw new Error('putImageLoadObject: imageLoadObject.cancelFn must be a function');
  }
  // 缓存需要记录当前是否被加载,时间戳等信息
  const cachedImage = {
    loaded: false,
    imageId,
    sharedCacheKey: undefined, // The sharedCacheKey for this imageId.  undefined by default
    imageLoadObject,
    timeStamp: Date.now(),
    sizeInBytes: 0
  };
  // 放入缓存
  imageCacheDict[imageId] = cachedImage;
  cachedImages.push(cachedImage);
  // 开始加载,因为缓存就不需要手动设定了,我们直接开始加载
  imageLoadObject.promise.then(function (image) {
    // 如果已经加载过了,直接返回
    if (cachedImages.indexOf(cachedImage) === -1) {
      // If the image has been purged before being loaded, we stop here.
      return;
    }
    // 否则开始加载
    cachedImage.loaded = true;
    cachedImage.image = image;

    if (image.sizeInBytes === undefined) {
      throw new Error('putImageLoadObject: image.sizeInBytes must not be undefined');
    }
    if (image.sizeInBytes.toFixed === undefined) {
      throw new Error('putImageLoadObject: image.sizeInBytes is not a number');
    }

    cachedImage.sizeInBytes = image.sizeInBytes;
    cacheSizeInBytes += cachedImage.sizeInBytes;

    const eventDetails = {
      action: 'addImage',
      image: cachedImage
    };

    triggerEvent(events, 'cornerstoneimagecachechanged', eventDetails);

    cachedImage.sharedCacheKey = image.sharedCacheKey;

    purgeCacheIfNecessary();
  }, () => {
    // 如果出现异常,会把字典内的相关对象删除
    const cachedImage = imageCacheDict[imageId];

    cachedImages.splice(cachedImages.indexOf(cachedImage), 1);
    delete imageCacheDict[imageId];
  });
}

函数首先进行一系列的判断逻辑,检查传入的imageId是否合法,之后会根据相关协议进行加载,加载完毕后发出cornerstoneimagecachechanged事件。

下面来看图片加载的代码:

export function loadImage (imageId, options) {
  if (imageId === undefined) {
    throw new Error('loadImage: parameter imageId must not be undefined');
  }
  // 去找缓存里有没有image对象
  const ImageLoadObject = getImageLoadObject(imageId);
  // 如果缓存里有image对象,就返回缓存里的image对象
  if (ImageLoadObject !== undefined) {
    return ImageLoadObject.promise;
  }
  // 否则执行加载
  return loadImageFromImageLoader(imageId, options).promise;
}

由于图片加载是异步的,则我们返回的是一个符合Promise A+Promise obj。而我们真正的图片加载是在getImageLoadObject(imageId)这样一个私有方法中做的,接下来会讲到。

loadImageFromImageLoader(imageId, options)

function loadImageFromImageLoader (imageId, options) {
  const colonIndex = imageId.indexOf(':');
  // 协议名称
  const scheme = imageId.substring(0, colonIndex);
  // 拿到对于特定协议的加载方法
  const loader = imageLoaders[scheme];
  // 如果找不到该协议的加载方法,则使用默认的加载方法
  if (loader === undefined || loader === null) {
    if (unknownImageLoader !== undefined) {
      return unknownImageLoader(imageId);
    }

    throw new Error('loadImageFromImageLoader: no image loader for imageId');
  }
  // 否则使用找到的加载方法进行加载
  const imageLoadObject = loader(imageId, options);

  // Broadcast an image loaded event once the image is loaded
  // 如果加载成功发出图片加载好了的事件,告知已经加载好图片,否则异常捕获,触发图片加载失败的事件
  imageLoadObject.promise.then(function (image) {
    triggerEvent(events, 'cornerstoneimageloaded', { image });
  }, function (error) {
    const errorObject = {
      imageId,
      error
    };

    triggerEvent(events, 'cornerstoneimageloadfailed', errorObject);
  });

  return imageLoadObject;
}

在示例中,我们的imageId会是example://1,这样的格式,这时,我们的scheme则是examplecolonIndex则是1,对于wado协议来说,例如传入的图片ID为wado://example.dicom.com/1.dcm,这样的格式,显然我们则会根据wado协议来进行加载。

这里需要注意的是对于正常加载和异常加载时,则会分别发出cornerstoneimageloadedcornerstoneimageloadfailed事件,这都是基于发布订阅模式的。

此外,如果没有找到对应协议的imageLoader,则会使用unknownImageLoader进行加载,这就是为什么我们可以注册unknownImageLoader方法的原因。