cornerstonejs / cornerstoneWADOImageLoader

[Deprecated] Use Cornerstone3D Instead https://cornerstonejs.org/
MIT License
285 stars 264 forks source link

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

Open faustmann opened 3 years ago

faustmann commented 3 years ago

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 commented 3 years ago

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:

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++
    }
  })
}
jamesdigid commented 11 months ago

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?

olivert1969 commented 1 month ago

same question - was @faustmann 's solution incorporated? thanks all

sofinan commented 2 weeks ago

Hello. We have a problem with OHIF zooming if instance has approximately 2000 frames - the loading all frames takes minimum 5 minutes because loading goes sequentially, frame by frame in separate requests. If we request all frames in one request it goes faster ~10 seconds. So, I am joining to previous posts about adding the solution to lib