Plugin to cache frame content (application/octet-stream)?

Hello.
Here is anonymized mammo study.

When i open this study on stone viewer, select series with many instance frames and scroll each frame is being retrieved slow(5-20 seconds). The same for OHIF, cornerstone js.

Is there any plugin to cache frame content? I can cache frames only for mammo studies, because mammo frames are 5-10mb and frames of other modalities are small enough to get them instantly, i don’t even see loading indicator when i scroll.

Hi,

What is your value for this configuration ?

  // Maximum size of the storage cache in MB.  The storage cache
  // is stored in RAM and contains a copy of recently accessed
  // files (written or read).  A value of "0" indicates the cache
  // is disabled.  (new in Orthanc 1.10.0)
  "MaximumStorageCacheSize" : 128,

HTH,

Alain

current value is 128. We upload many studies with thousand of frames an hour. 8 gb of ram can contain only last 1 such mammo study. Is there a way to store frame content in a disk, not in ram? maybe in swap?

No

But have you tried increasing the value ? Caching can happen when viewing to avoid the whole file being transcoded every time you want to extract a single frame.

I set to 8000. In docker stats i can see that it increases to 6-8 gb when i open the study. But frames still are being retrieved in 5-8 seconds even for 1 instance that has 123 frames in OHIF.
May be OHIF uses dicom-web and dicom-web plugin doesn;t care about MaximumStorageCacheSize?

I actually wrote some plugin, it works good. But i have some questions in code comments:

dir_path = os.path.dirname(os.path.realpath(__file__))

def OnChange(changeType, level, resource):
    if changeType == orthanc.ChangeType.NEW_INSTANCE:
        newInstance = json.loads(orthanc.RestApiGet('/instances/%s' % resource))

        query = {}
        query['Expand'] = True
        query['Level'] = "Instance"
        query['Query'] = { 'SOPInstanceUID': newInstance['MainDicomTags']['SOPInstanceUID'] }
        query['RequestedTags'] = ["StudyInstanceUID", "SeriesInstanceUID"]
        instanceMetadata = json.loads(orthanc.RestApiPost('/tools/find', json.dumps(query)))

        StudyInstanceUID = instanceMetadata[0]['RequestedTags']['StudyInstanceUID']
        SeriesInstanceUID = instanceMetadata[0]['RequestedTags']['SeriesInstanceUID']
        SOPInstanceUID = newInstance['MainDicomTags']['SOPInstanceUID']
        NumberOfFrames = newInstance['MainDicomTags'].get('NumberOfFrames', None)

        if(NumberOfFrames == None):
            return

        for i in range(int(NumberOfFrames)):
            frame = i

            # b = requests.get(f'http://localhost:8042/dicom-web/studies/{StudyInstanceUID}/series/{SeriesInstanceUID}/instances/{SOPInstanceUID}/frames/{frame}',
                # auth=(os.getenv('ORTHANC_USERNAME'), os.getenv('ORTHANC_PASSWORD')))

            # this works a lot faster than dicom-web above. Do you know why? any caveats? maybe because dicom-web plugin doesn't care about MaximumStorageCacheSize?
            b = requests.get(f'http://localhost:8042/instances/{resource}/frames/{frame}/raw',
                             auth=(os.getenv('ORTHANC_USERNAME'), os.getenv('ORTHANC_PASSWORD')))


            cacheFolder = os.path.join(dir_path, f'../python_tmp/cache/{StudyInstanceUID}/{SeriesInstanceUID}/{SOPInstanceUID}')
            os.makedirs(cacheFolder, exist_ok=True)
            file_path = os.path.join(cacheFolder, str(frame + 1))

            with open(file_path, 'wb') as file:
                file.write(b.content)
orthanc.RegisterOnChangeCallback(OnChange)

It creates study/series/instance/1-2-3 frames files.

Here is route to serve them

def OnRest(output, uri, **request):
    res = 'ok'
    pattern = r"/dicom/studies/([^/]+)/series/([^/]+)/instances/([^/]+)/frames/([^/]+)"
    print(1, uri)
    match = re.search(pattern, uri)
    if match:
        StudyInstanceUID = match.group(1)
        SeriesInstanceUID = match.group(2)
        SOPInstanceUID = match.group(3)
        frame = match.group(4)

    cacheFolder = os.path.join(dir_path, f'../python_tmp/cache/{StudyInstanceUID}/{SeriesInstanceUID}/{SOPInstanceUID}')

    with open(cacheFolder + f'/{str(frame)}', 'rb') as f:
        res = f.read()

    output.AnswerBuffer(res, 'application/octet-stream')
    # output.AnswerBuffer(res, 'multipart/related; type="application/octet-stream; transfer-syntax=1.2.840.10008.1.2.1"; boundary=e156a009-a9fc-4206-bd96-6180955b3089-b8509407-a64b-4d42-a78b-95269a60f')
    # output.AnswerBuffer(res, 'application/gzip')

  # which one from above has correct content type? Every of them works. is boundary necessary?

orthanc.RegisterRestCallback('/dicom/(.*)', OnRest)

When i scroll i don’t even see loading indicator in OHIF. If plugin has some caveats or improvements, please tell me where. I’m going to implement auto deleting of old studies.
Thanks

Hi,

Yes, your plugin seems correct to me. Nice one !

Note that, on my side, I have just implemented a patch in the dicom-web plugin that greatly improves the performances.

This patches uses a more efficient route to download a single frame that does not need transcoding as your plugin does.

On my setup, the loading time of a single frame is reduced from 16s to 2s and the total loading time for 69 frames from 55s to 12s

Before:

After:

This will be available in the mainline binaries before being included in the next official release.

Best regards,

Alain.

Sounds good. Hope it will be in orthancteam/orthanc image’s release notes so i could know when to upgrade

I set image: orthancteam/orthanc-pre-release:master-full-unstable and it really works fast. Since clinic’s pc with Orthanc is not powerful enough to use my implementation with caching i’m gonna use full-unstable version for now. Is it too risky? If this feature will be in stable version in a week i think we can wait. Thanks again

No, it has been tested against most of our integration tests.

Unfortunately, that won’t be released next week.

Alain