cornerstoneWADOImageLoader icon indicating copy to clipboard operation
cornerstoneWADOImageLoader copied to clipboard

Is it possible to load a multiframe DICOM via WADO-RS and the BulkDataURI

Open faustmann opened this issue 4 years ago • 4 comments

Hi,

thanks for the useful project. :) I have the following question:

I want to load multi-frame DICOMs via WADO-RS. Each DICOM contains a lot of frames. Thus fetching every single frame with the HTTP request ..../frames/{frameNumber} from the Orthanc - DICOM Server is quite slow.

The example WADO-URI (DICOM P10 via HTTP GET) multiframe (https://github.com/cornerstonejs/cornerstoneWADOImageLoader/blob/master/examples/wadourimultiframe/index.html) loads everything at once. The WADO-URI approach has the dataSetCacheManager (https://github.com/cornerstonejs/cornerstoneWADOImageLoader/blob/00cf0419c98c6bddef121d4409c8a2a9d780f336/examples/wadourimultiframe/index.html#L112) for this case.

Is it also possible to achieve something similar within the WADO-RS approach? For example to start with fetching the bulk data with the URI in the DICOM attribute 7FE00010.

faustmann avatar Jun 16 '21 13:06 faustmann

I solved this in the following way: I asked the Orthanc Team if there is a way to speed this up. They gave me the good hint to request multiple frames. ( link to hint )

In orthanc it is possible to request multiple frame with the syntax .../studies/../series/../instances/../frames/1,2,3. I found no good way to handle the response of such a request via a custom ImageLoader so I wrote a function that fetches the frames and adds the frame into the cornerstone cache directly. In the added snipped the function cacheAllDICOMFrames does this job. This function requires two parameters:

  • dicomRestId: link to the dicom instance like .../studies/../series/../instances/..
  • listOfRequestedFrameNumbers: list of requested frames like [1,2,3]

Currently, it is quite tailored to our needs and not written for general purposes but maybe it is useful for someone. If there is interest to integrate this into this project then with some guidance I am happy to help. It uses functions from the wadors part of this library and the function cacheAllDICOMFrames is inspired from the function loadImage of wadors.

const findBoundary = (header) => {
  for (let i = 0; i < header.length; i++) {
    if (header[i].substr(0, 2) === '--') {
      return header[i];
    }
  }
}

const findContentType = (header) => {
  for (let i = 0; i < header.length; i++) {
    if (header[i].substr(0, 13) === 'Content-Type:') {
      return header[i].substr(13).trim();
    }
  }
}

const uint8ArrayToString = (data, offset, length) => {
  offset = offset || 0;
  length = length || data.length - offset;
  let str = '';

  for (let i = offset; i < offset + length; i++) {
    str += String.fromCharCode(data[i]);
  }

  return str;
}

const cacheAllDICOMFrames = (dicomRestId, listOfRequestedFrameNumbers) => {
  const strOfAllRequestedFrames = listOfRequestedFrameNumbers.join()

  const requestUrl = `${dicomRestId}/frames/${strOfAllRequestedFrames}`

  const start = new Date().getTime()

  cornerstoneWADOImageLoader.internal.xhrRequest(requestUrl, requestUrl, {
    accept: 'multipart/related; type="application/octet-stream"; transfer-syntax=*'
  }).then(imageFrameAsArrayBuffer => {
    // request succeeded, Parse the multi-part mime response
    const response = new Uint8Array(imageFrameAsArrayBuffer);

    // First look for the multipart mime header
    const tokenIndex = cornerstoneWADOImageLoader.wadors.findIndexOfString(response, '\r\n\r\n');
    if (tokenIndex === -1) {
      new Error('invalid response - no multipart mime header');
    }
    const header = uint8ArrayToString(response, 0, tokenIndex);

    // Now find the boundary  marker
    const split = header.split('\r\n');
    const boundary = findBoundary(split);
    if (!boundary) {
      new Error('invalid response - no boundary marker');
    }

    let currentStartIndex = tokenIndex + 4 // skip over the \r\n\r\n
    let currentEndIndex
    let frameIndex = 0

    while (frameIndex < listOfRequestedFrameNumbers.length &&
      currentStartIndex !== -1 &&
      (currentEndIndex = cornerstoneWADOImageLoader.wadors.findIndexOfString(response, boundary, currentStartIndex)) !== -1) {

      // Remove \r\n from the length
      const length = currentEndIndex - currentStartIndex - 2;

      const imageId = `wadors:${dicomRestId}/frames/${listOfRequestedFrameNumbers[frameIndex]}`

      const imageData = new Uint8Array(imageFrameAsArrayBuffer, currentStartIndex, length)

      const promise = new Promise((resolve, reject) => {
        const imagePromise = cornerstoneWADOImageLoader.createImage(
          imageId,
          imageData,
          findContentType(split).split(';')[1].split('=')[1])

        imagePromise.then(image => {
          // add the loadTimeInMS property
          const end = new Date().getTime();

          image.loadTimeInMS = end - start;
          resolve(image);
        }, reject)
      })

      cornerstone.imageCache.putImageLoadObject(imageId, {
        promise,
        cancelFn: undefined,
      })

      currentStartIndex = cornerstoneWADOImageLoader.wadors.findIndexOfString(response, '\r\n\r\n', currentEndIndex) + 4
      frameIndex++
    }
  })
}

faustmann avatar Sep 06 '21 09:09 faustmann

Are there any plans to add the above solution to this library? It solves a use case I'm currently working on with OHIF.

@faustmann Do you have a fork or PR for the above solution?

jamesdigid avatar Dec 26 '23 17:12 jamesdigid