sjoerdk/dicomtrolley

View on GitHub
docs/usage.md

Summary

Maintainability
Test Coverage
# Usage

Shows how to do things in code. For information on the structure see [concepts](concepts.md)

## Basic example
```python
# Create a http session
session = requests.Session()

# Use this session to create a trolley using MINT and WADO
trolley = Trolley(searcher=Mint(session, "https://server/mint"),
                  downloader=WadoURI(session, "https://server/wado_uri"))

# find some studies
studies = trolley.find_studies(Query(PatientName='B*'))

# download the first one
trolley.download(studies[0], output_dir='/tmp/trolley')
```

## Finding studies
```python
# Find all studies for patients starting with 'B'
studies = trolley.find_studies(Query(PatientName='B*'))
```

```python
# Find CT studies from the first three days of March 2020
# Include patient birth date and referring physician in results
studies = trolley.find_studies(
    Query(ModalitiesInStudy='CT*',                
          min_study_date=datetime(year=2020, month=3, day=1),
          max_study_date=datetime(year=2020, month=3, day=3),
          include_fields=['PatientBirthDate', 'ReferringPhysicianName']))
```

For details on Query parameters see [`Query`](concepts.md#Query)

## Finding series and instance details
To include series and instance level information as well, use the [`queryLevel`](concepts.md#query_level) parameter

```python
studies = trolley.find_studies(      # find studies series and instances
    Query(StudyInstanceUID='B*', 
          query_level=QueryLevels.INSTANCE))

a_series = studies[0].series[0]         # studies now contain series    
an_instance = a_series.instances[0]     # and series contain instances
```
Data sent back by the server is parsed in a DICOM object hierarchy. Each object stores its additional data in the 
`data` field. This field is a [pydicom.Dataset](
https://pydicom.github.io/pydicom/stable/reference/generated/pydicom.dataset.Dataset.html) 
object and can be addressed as such:
```python
an_instance.data            # {pydicom.Dataset} instance
an_instance.data.Rows       # {int} 100
an_instance.data["Rows"]    # {DataElement} instance
```
!!! note
    The information sent back for each DICOM object level depends on the [query_level](concepts.md#query_level) and
    [include_fields](concepts.md#include_fields) parameters, as well as on the server type and configuration
## Downloading images
Any study, series or instance can be downloaded (see [DICOMObject](concepts.md#dicomobject)):
```python
studies = trolley.find_studies(Query(PatientName='B*',
                                     query_level=QueryLevels.INSTANCE))

trolley.download(studies, '/tmp')                             # all studies
trolley.download(studies[0], '/tmp')                          # a single study
trolley.download(studies[0].series[0], '/tmp')                # a single series
trolley.download(studies[0].series[0].instances[:3], '/tmp')  # first 3 instances
```

You can also download [DICOM object references][dicomtrolley.core.DICOMObjectReference] based on id directly:
```python
trolley.download(StudyReference(study_uid="1.1"), "/tmp")
trolley.download(SeriesReference(study_uid="1.1", 
                                 series_uid='2.2'), "/tmp")
trolley.download(InstanceReference(study_uid="1.1", 
                                   series_uid='2.2', 
                                   instance_uid='3.3'), "/tmp")
```

## Downloading datasets
More control over download: obtain `pydicom.Dataset` instances directly 

```python
studies = trolley.find_studies(                 # find study including instances
    Query(PatientID='1234', 
          query_level=QueryLevels.INSTANCE))

for ds in trolley.fetch_all_datasets(studies):  # obtain Dataset for each instance
    ds.save_as(f'/tmp/{ds.SOPInstanceUID}.dcm')
```

## Protocols
Have a look at the [downloader](concepts.md#downloader) and [searcher](concepts.md#searcher) implementations.  


## Download format
By default, trolley writes downloads to disk as `StudyID/SeriesID/InstanceID`, sorting files into separate
study and series folders. You can change this by passing a [DICOMDiskStorage][dicomtrolley.storage.DICOMDiskStorage] 
instance to trolley:

```python
from dicomtrolley.storage import FlatStorageDir

#  Creates no sub-folders, just write to single flat file
storage = FlatStorageDir(path='/tmp')

trolley = Trolley(searcher=a_searcher, downloader=a_downloader, storage=storage)
```

You can create your own custom storage method by subclassing 
[DICOMDiskStorage][dicomtrolley.storage.DICOMDiskStorage]:

```python
from dicomtrolley.storage import DICOMDiskStorage

class MyStorage(DICOMDiskStorage):
  """Saves to unique uid filename"""
  def save(self, dataset, path):    
    dataset.save_as(Path(path) / uuid.uuid4())

trolley = Trolley(searcher=a_searcher, downloader=a_downloader, storage=MyStorage())

```
## Caching
You can add caching to any [`Searcher`](concepts.md#searcher) by wrapping it with
a [CachedSearcher][dicomtrolley.caching.CachedSearcher] instance:

```python
from dicomtrolley.caching import CachedSearcher, DICOMObjectCache

searcher = CachedSearcher(searcher=a_searcher, 
                          cache=DICOMObjectCache(expiry_seconds=300))

trolley = Trolley(searcher=searcher, downloader=a_downloader)
```

[CachedSearcher][dicomtrolley.caching.CachedSearcher] is a [Searcher][dicomtrolley.core.Searcher]
and can be used like any other. It will return cached results to any of its function
calls for up to `expiry_seconds` seconds.

## Logging
Dicomtrolley uses the standard [logging](https://docs.python.org/3/library/logging.html) module. The root logger is 
called `trolley`. To print log messages, add the following to your code:

```python
import logging
logging.basicConfig(level=logging.DEBUG)

# get the root logger to set specific properties
root_logger = logging.getLogger('trolley')
```


## Ignoring RAD69 errors
By default, any error returned by a rad69 server will raise an exception, halting any further download. To ignore 
certain errors and continue the download, pass the exception class to the Rad69 constructor:

```python
from dicomtrolley.rad69 import XDSMissingDocumentError
trolley = Trolley(searcher=a_searcher, 
                  downloader=Rad69(session=requests.session(),
                                   url="https://server/rad69",
                                   errors_to_ignore = [XDSMissingDocumentError]))
# trolley.download() # will now skip series raising XDSMissingDocumentError
```

## Authentication
See [authentication](authentication.md)