DICOM Web Services (DICOMweb)

Section 17 of 27
63% complete

DICOMweb provides RESTful APIs for DICOM operations, enabling web-based medical imaging applications.

DICOMweb Overview

DICOMweb Servicessaravanansubramanian.comthree HTTP services — one per major DIMSE operation, all speaking RESTQIDO-RSQueryDIMSE equivalentC-FINDHTTPGET with query parametersReturnsJSON or XMLWADO-RSRetrieveDIMSE equivalentC-MOVE / C-GETHTTPGET for studies / series / instancesReturnsDICOM, bulk data, or renderedSTOW-RSStoreDIMSE equivalentC-STOREHTTPPOST with multipart contentReturnsstorage results as JSON Traditional DICOM to DICOMwebsaravanansubramanian.comhow each DIMSE service and its transport map to a RESTful equivalentTRADITIONAL DICOMDICOMwebC-FINDquery for studies / series / instancesQIDO-RSGET with query parametersC-MOVE / C-GETretrieve images to a destinationWADO-RSGET for DICOM, metadata, or renderedC-STOREpush an instance into the archiveSTOW-RSPOST multipart / relatedTCP port 104 · binary protocolHTTP / HTTPS 80 / 443 · JSON or XMLboth share the same information model (Study / Series / Instance UIDs)

QIDO-RS (Query)

QIDO-RS queries for studies, series, or instances using HTTP GET.

Query Studies

The following QIDO client demonstrates querying for studies using standard HTTP requests. The query parameters map to DICOM attributes, and results are returned in DICOM JSON format. Pagination support enables handling large result sets.

public class QidoClient
{
    private readonly HttpClient httpClient;
    private readonly string baseUrl;

    public QidoClient(string baseUrl)
    {
        this.baseUrl = baseUrl.TrimEnd('/');
        httpClient = new HttpClient();
        httpClient.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/dicom+json"));
    }

    public async Task<List<StudyResult>> QueryStudies(
        string patientName = null,
        string patientID = null,
        string studyDate = null,
        string modality = null,
        int limit = 100,
        int offset = 0)
    {
        // Build query URL
        var queryParams = new List<string>();

        if (!string.IsNullOrEmpty(patientName))
            queryParams.Add($"PatientName={Uri.EscapeDataString(patientName)}");
        if (!string.IsNullOrEmpty(patientID))
            queryParams.Add($"PatientID={Uri.EscapeDataString(patientID)}");
        if (!string.IsNullOrEmpty(studyDate))
            queryParams.Add($"StudyDate={studyDate}");
        if (!string.IsNullOrEmpty(modality))
            queryParams.Add($"ModalitiesInStudy={modality}");

        queryParams.Add($"limit={limit}");
        queryParams.Add($"offset={offset}");

        // Include return fields
        queryParams.Add("includefield=StudyInstanceUID");
        queryParams.Add("includefield=PatientName");
        queryParams.Add("includefield=PatientID");
        queryParams.Add("includefield=StudyDate");
        queryParams.Add("includefield=StudyDescription");
        queryParams.Add("includefield=ModalitiesInStudy");
        queryParams.Add("includefield=NumberOfStudyRelatedSeries");
        queryParams.Add("includefield=NumberOfStudyRelatedInstances");

        string url = $"{baseUrl}/studies?{string.Join("&", queryParams)}";

        HttpResponseMessage response = await httpClient.GetAsync(url);
        response.EnsureSuccessStatusCode();

        string json = await response.Content.ReadAsStringAsync();
        return ParseStudyResults(json);
    }

    private List<StudyResult> ParseStudyResults(string json)
    {
        var results = new List<StudyResult>();

        // Parse DICOM JSON format
        var jsonArray = JsonConvert.DeserializeObject<JArray>(json);

        foreach (JObject item in jsonArray)
        {
            results.Add(new StudyResult
            {
                StudyInstanceUID = GetDicomValue(item, "0020000D"),
                PatientName = GetDicomValue(item, "00100010"),
                PatientID = GetDicomValue(item, "00100020"),
                StudyDate = GetDicomValue(item, "00080020"),
                StudyDescription = GetDicomValue(item, "00081030"),
                Modality = GetDicomValue(item, "00080061"),
                NumberOfSeries = GetDicomValue(item, "00201206"),
                NumberOfInstances = GetDicomValue(item, "00201208")
            });
        }

        return results;
    }

    private string GetDicomValue(JObject item, string tag)
    {
        var element = item[tag];
        if (element == null) return "";

        var value = element["Value"];
        if (value == null || !value.HasValues) return "";

        return value[0]?.ToString() ?? "";
    }
}

public class StudyResult
{
    public string StudyInstanceUID { get; set; }
    public string PatientName { get; set; }
    public string PatientID { get; set; }
    public string StudyDate { get; set; }
    public string StudyDescription { get; set; }
    public string Modality { get; set; }
    public string NumberOfSeries { get; set; }
    public string NumberOfInstances { get; set; }
}

Query Series and Instances

Hierarchical queries drill down from studies to series to instances. Each level uses the parent’s UID to narrow the scope. These endpoints follow RESTful URL patterns with UIDs in the path.

public async Task<List<SeriesResult>> QuerySeries(string studyInstanceUID)
{
    string url = $"{baseUrl}/studies/{studyInstanceUID}/series";
    HttpResponseMessage response = await httpClient.GetAsync(url);
    response.EnsureSuccessStatusCode();

    string json = await response.Content.ReadAsStringAsync();
    // Parse and return series results
    return ParseSeriesResults(json);
}

public async Task<List<InstanceResult>> QueryInstances(
    string studyInstanceUID,
    string seriesInstanceUID)
{
    string url = $"{baseUrl}/studies/{studyInstanceUID}/series/{seriesInstanceUID}/instances";
    HttpResponseMessage response = await httpClient.GetAsync(url);
    response.EnsureSuccessStatusCode();

    string json = await response.Content.ReadAsStringAsync();
    // Parse and return instance results
    return ParseInstanceResults(json);
}

WADO-RS (Retrieve)

WADO-RS retrieves DICOM objects, metadata, or rendered images.

Retrieve Studies/Series/Instances

WADO-RS supports multiple retrieval modes: raw DICOM, metadata-only, rendered images, and thumbnails. Use the Accept header to specify desired format. Rendered image retrieval enables direct web display without client-side DICOM decoding.

public class WadoClient
{
    private readonly HttpClient httpClient;
    private readonly string baseUrl;

    public WadoClient(string baseUrl)
    {
        this.baseUrl = baseUrl.TrimEnd('/');
        httpClient = new HttpClient();
    }

    // Retrieve entire study as DICOM
    public async Task<List<byte[]>> RetrieveStudy(string studyInstanceUID)
    {
        string url = $"{baseUrl}/studies/{studyInstanceUID}";

        var request = new HttpRequestMessage(HttpMethod.Get, url);
        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(
            "multipart/related",
            new NameValueHeaderValue("type", "\"application/dicom\"")));

        HttpResponseMessage response = await httpClient.SendAsync(request);
        response.EnsureSuccessStatusCode();

        return await ParseMultipartDicom(response);
    }

    // Retrieve single instance as DICOM
    public async Task<byte[]> RetrieveInstance(
        string studyInstanceUID,
        string seriesInstanceUID,
        string sopInstanceUID)
    {
        string url = $"{baseUrl}/studies/{studyInstanceUID}" +
            $"/series/{seriesInstanceUID}/instances/{sopInstanceUID}";

        var request = new HttpRequestMessage(HttpMethod.Get, url);
        request.Headers.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/dicom"));

        HttpResponseMessage response = await httpClient.SendAsync(request);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadAsByteArrayAsync();
    }

    // Retrieve rendered image (PNG/JPEG)
    public async Task<byte[]> RetrieveRenderedImage(
        string studyInstanceUID,
        string seriesInstanceUID,
        string sopInstanceUID,
        int? frame = null,
        string quality = null)
    {
        string url = $"{baseUrl}/studies/{studyInstanceUID}" +
            $"/series/{seriesInstanceUID}/instances/{sopInstanceUID}/rendered";

        var queryParams = new List<string>();
        if (frame.HasValue)
            queryParams.Add($"frame={frame.Value}");
        if (!string.IsNullOrEmpty(quality))
            queryParams.Add($"quality={quality}");

        if (queryParams.Any())
            url += "?" + string.Join("&", queryParams);

        var request = new HttpRequestMessage(HttpMethod.Get, url);
        request.Headers.Accept.Add(
            new MediaTypeWithQualityHeaderValue("image/png"));

        HttpResponseMessage response = await httpClient.SendAsync(request);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadAsByteArrayAsync();
    }

    // Retrieve thumbnail
    public async Task<byte[]> RetrieveThumbnail(
        string studyInstanceUID,
        string seriesInstanceUID,
        string sopInstanceUID)
    {
        string url = $"{baseUrl}/studies/{studyInstanceUID}" +
            $"/series/{seriesInstanceUID}/instances/{sopInstanceUID}/thumbnail";

        var request = new HttpRequestMessage(HttpMethod.Get, url);
        request.Headers.Accept.Add(
            new MediaTypeWithQualityHeaderValue("image/jpeg"));

        HttpResponseMessage response = await httpClient.SendAsync(request);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadAsByteArrayAsync();
    }

    // Retrieve metadata only (no pixel data)
    public async Task<string> RetrieveMetadata(
        string studyInstanceUID,
        string seriesInstanceUID,
        string sopInstanceUID)
    {
        string url = $"{baseUrl}/studies/{studyInstanceUID}" +
            $"/series/{seriesInstanceUID}/instances/{sopInstanceUID}/metadata";

        var request = new HttpRequestMessage(HttpMethod.Get, url);
        request.Headers.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/dicom+json"));

        HttpResponseMessage response = await httpClient.SendAsync(request);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadAsStringAsync();
    }

    private async Task<List<byte[]>> ParseMultipartDicom(HttpResponseMessage response)
    {
        var results = new List<byte[]>();

        var content = response.Content;
        var multipartContent = await content.ReadAsMultipartAsync();

        foreach (var part in multipartContent.Contents)
        {
            byte[] dicomBytes = await part.ReadAsByteArrayAsync();
            results.Add(dicomBytes);
        }

        return results;
    }
}

STOW-RS (Store)

STOW-RS stores DICOM objects using HTTP POST.

Store DICOM Objects

STOW-RS uploads DICOM objects using HTTP POST with multipart content. Multiple instances can be uploaded in a single request for efficiency. The server responds with a JSON document indicating success or failure for each instance.

public class StowClient
{
    private readonly HttpClient httpClient;
    private readonly string baseUrl;

    public StowClient(string baseUrl)
    {
        this.baseUrl = baseUrl.TrimEnd('/');
        httpClient = new HttpClient();
    }

    public async Task<StowResponse> StoreInstances(List<string> dicomFilePaths)
    {
        string url = $"{baseUrl}/studies";

        // Create multipart content
        var multipartContent = new MultipartContent("related",
            $"----Boundary{Guid.NewGuid():N}");
        multipartContent.Headers.ContentType.Parameters.Add(
            new NameValueHeaderValue("type", "\"application/dicom\""));

        foreach (string filePath in dicomFilePaths)
        {
            byte[] dicomBytes = File.ReadAllBytes(filePath);

            var dicomContent = new ByteArrayContent(dicomBytes);
            dicomContent.Headers.ContentType =
                new MediaTypeHeaderValue("application/dicom");

            multipartContent.Add(dicomContent);
        }

        HttpResponseMessage response = await httpClient.PostAsync(url, multipartContent);

        string responseJson = await response.Content.ReadAsStringAsync();

        return new StowResponse
        {
            Success = response.IsSuccessStatusCode,
            StatusCode = (int)response.StatusCode,
            ResponseBody = responseJson
        };
    }

    public async Task<StowResponse> StoreInstance(byte[] dicomBytes)
    {
        string url = $"{baseUrl}/studies";

        var content = new ByteArrayContent(dicomBytes);
        content.Headers.ContentType = new MediaTypeHeaderValue("application/dicom");

        HttpResponseMessage response = await httpClient.PostAsync(url, content);

        string responseJson = await response.Content.ReadAsStringAsync();

        return new StowResponse
        {
            Success = response.IsSuccessStatusCode,
            StatusCode = (int)response.StatusCode,
            ResponseBody = responseJson
        };
    }
}

public class StowResponse
{
    public bool Success { get; set; }
    public int StatusCode { get; set; }
    public string ResponseBody { get; set; }
}

DICOMweb URL Patterns

DICOMweb URL Structure
======================

Base URL: https://dicom.example.com/dicomweb

QIDO-RS (Query):
  GET /studies?PatientName=Doe*&StudyDate=20240115
  GET /studies/{studyUID}/series
  GET /studies/{studyUID}/series/{seriesUID}/instances

WADO-RS (Retrieve):
  GET /studies/{studyUID}                              → All DICOM
  GET /studies/{studyUID}/series/{seriesUID}           → Series DICOM
  GET /studies/{studyUID}/.../instances/{instanceUID}  → Instance
  GET /studies/{studyUID}/.../instances/{uid}/metadata → JSON metadata
  GET /studies/{studyUID}/.../instances/{uid}/rendered → PNG/JPEG
  GET /studies/{studyUID}/.../instances/{uid}/frames/1 → Specific frame

STOW-RS (Store):
  POST /studies                    → Store to any study
  POST /studies/{studyUID}         → Store to specific study

Usage Examples

These examples demonstrate typical DICOMweb workflows including searching for studies, retrieving rendered images for web display, and uploading new DICOM files to a server.

// QIDO-RS: Find studies
var qidoClient = new QidoClient("https://dicom.example.com/dicomweb");
var studies = await qidoClient.QueryStudies(
    patientName: "Doe*",
    studyDate: "20240101-20240131",
    modality: "CT"
);

Console.WriteLine($"Found {studies.Count} studies");
foreach (var study in studies)
{
    Console.WriteLine($"  {study.PatientName} - {study.StudyDate} - {study.StudyDescription}");
}

// WADO-RS: Retrieve rendered image for web display
var wadoClient = new WadoClient("https://dicom.example.com/dicomweb");
byte[] imageBytes = await wadoClient.RetrieveRenderedImage(
    studies[0].StudyInstanceUID,
    seriesUID,
    instanceUID
);
File.WriteAllBytes("image.png", imageBytes);

// STOW-RS: Upload DICOM files
var stowClient = new StowClient("https://dicom.example.com/dicomweb");
var storeResult = await stowClient.StoreInstances(
    new List<string> { "image1.dcm", "image2.dcm" }
);
Console.WriteLine($"Storage {(storeResult.Success ? "succeeded" : "failed")}");

Benefits of DICOMweb

AspectTraditional DICOMDICOMweb
ProtocolCustom TCPHTTP/HTTPS
FirewallPort 104 neededStandard web ports
Load BalancingComplexStandard web LB
AuthenticationAE TitleOAuth, JWT, SAML
CachingCustomHTTP caching
Web IntegrationComplexNative REST

DICOMweb enables modern web-based medical imaging applications with standard web technologies.

Quiz: DICOM Web Services (DICOMweb)

Question 1 of 4

What does QIDO-RS stand for?