Skip to content

WIP - feat/extended-download-dicomweb #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

TFRadicalImaging
Copy link
Collaborator

@TFRadicalImaging TFRadicalImaging commented Mar 28, 2025

🚀 Add download Command: Full DICOM Study Downloader via DICOMweb

This PR introduces a new CLI command download that downloads an entire DICOM study—including study, series, instances, frames, and bulkdata—from a DICOMweb-compliant service.

The feature supports advanced functionality such as intelligent folder structuring, multi-frame handling, and JSON compression by default.


🚀 Features

  • 📥 Download a full DICOM study including:
    • Study metadata
    • Series metadata
    • DICOM instances
    • Frames (multi-frame objects)
    • Bulk data (e.g., waveforms, PDFs, encapsulated binaries)
  • 💾 Save everything locally in a clean, directory-based structure (SDW: Static DICOM Web)
  • 🔍 Verbose output to show real-time progress (ideal for long studies)
  • 🔌 Pluggable architecture for multiple DICOM sources (currently DICOMweb only)

🧪 Usage

node cli.js download <url> --StudyInstanceUID <UID> [options]
node cli.js download https://d14fa38qiwhyfd.cloudfront.net/dicomweb \
  --StudyInstanceUID 1.2.276.0.7230010.3.1.2.2155604110.4180.1021041295.21 \
  --directory ~/dcmjs

🛠️ Options

Option Description
<url> DICOMweb endpoint (must start with http)
-S, --StudyInstanceUID The StudyInstanceUID you want to download
-d, --directory Target folder for saving (default: ./)
--zip-json Save JSON metadata as compressed .json.gz
--debug Enable debug logging (more verbose output)

🗂️ Directory Structure (SDW Format)

downloads/
└── <StudyInstanceUID>/
    ├── index.json                      # Study-level metadata
    ├── series/
    │   ├── index.json                  # List of series
    │   └── <SeriesInstanceUID>/
    │       ├── metadata                # Series-level metadata
    │       └── instances/
    │           ├── index.json          # List of instances
    │           └── <SOPInstanceUID>/
    │               ├── index.json      # Instance metadata
    │               ├── frames/         # Frame pixel data
    │               └── metadata/       # Optional frame metadata
    └── bulkdata/
        └── <hashed-folder>/
            └── <hash>.json             # Base64-encoded binary content

🧱 Architecture

  • cliDownload.js — CLI command registration and main workflow
  • DicomAccess.js — Abstract class defining a common interface
  • DicomWebAccess.js — Implements fetching data from DICOMweb
  • DicomWebSeriesAccess.js / DicomWebStudyAccess.js — Handle specific study/series queries
  • DicomStoreSDW.js — Stores metadata and binary data in a structured format
  • DicomStoreAccess.js — Abstract base for pluggable storage backends

📅 Future Plans

  • Support for:
    • DIMSE C-GET/C-MOVE
    • Local DICOMDIR parsing
  • Upload (STOW-RS) and query (QIDO-RS) features
  • UI integration or Electron wrapper

@TFRadicalImaging TFRadicalImaging changed the title Feat/extended download dicomweb WIP - feat/extended-download-dicomweb Mar 28, 2025
@wayfarer3130
Copy link
Collaborator

For this change, what I want is a generic interface for reading and writing DICOM data, so that it can read and write to any/all:
DICOMweb - using the dicomweb-client dcmjs library
DIMSE - using the dicom-dimse library
File - static dicomweb format - using the fs library
File - DICOMDir format - using the dcmjs library and the fs library for reading/writing

You don't have to IMPLEMENT all of these, the only ones required right now are reading from DICOMweb and store to SDW file format, but it should go through a class interface to set things up. I would suggest testing with the SDW file for reading as well as writing, but the eventual goal is to have a single implementation that handles various endpoints.

@wayfarer3130
Copy link
Collaborator

wayfarer3130 commented Apr 4, 2025

export class DicomAccess {
public static createInstance(url, options) {
if (url.startsWith("scp:")) {
return new ScpDicomAccess(url, options);
}
if (url.startsWith("http")) {
return new DicomWebAccess(url, options);
}
return new FileDicomAccess(url, options);
}

public queryStudy?(studyInstanceUID: string): Promise;
public storeStudy(studyAccess: DicomStudyAccess, options): Promise;
}

export class DicomStudyAccess {
public abstract getPatientData();
public abstract getStudyData();

// Not quite sure yet what these will all return, as it should work for dimse
// dicomweb, local dicomweb file system, local dicom part 10 directories (DICOMDIR?)
public iterateSeries(options): Iterable;

public storeSeries(seriesAccess: SeriesAccess) {
// Call the series level store metadata
}
}

export abstract class DicomSeriesAccess {
public getMetadata?(): object[];

/** Stores the data in the preferred format for the given output location */
public storeInstance?(imageAccess, options);
}

// Implementation for DICOMweb (separate file)
// Should use library dicomweb-client to query back end data

export class DicomWebAccess extends DicomAccess {
constructor(dicomWebUrl) {
// stuff here to create a dicomweb
}

// Implementation of DicomAccess in terms of the library dicomweb-client
}

// Dimse implementation (TODO later, but think about this one as to how to make it work consistently.)
export class DimseAccess extends DicomAccess {
// Implementation in terms of dicom-dimse library
}

// File implementation
export class FileDicomAccess extends DicomAccess {
// Implementation in terms of local file system - should use async returns

public async queryStudy(studyInstanceUID: string) {
// Read the file index.json data and return a FileDicomStudyAccess instance
}
}

export class FileStudyAccess extends DicomStudyAccess {
// Implement storeMetadata
}

…ecture and SDW output

- Supports full DICOM study download via DICOMweb (WADO-RS)
- Downloads study metadata, series metadata, instances, frames, and bulk data
- Saves to local disk using SDW structure (Simple DICOM Web)
- Implements pluggable DicomAccess interface (currently DICOMweb only)
- Provides clear console logs for user feedback
- Includes robust file organization and metadata serialization
- Ready for future support of DIMSE and DICOMDIR backends
// Abstract base class for DICOM access implementations
export class DicomAccess {
static async createInstance(url, options) {
if (url.startsWith("http")) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to have at least two implementations - local dicomweb file system and dicomweb http as this should work for both types.

import * as dicomweb from "../dicomweb.js";
import { createDicomWebConfig, fixBulkDataURI } from "../utils/index.js";

const nonFrameSOPs = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please extract a method to test if an instance is a non image instance.

import { saveJson } from "../utils/index.js";
import { DicomStoreAccess } from "./DicomStoreAccess.js";

export class DicomStoreSDW extends DicomStoreAccess {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The store should be a full, first class DicomAccess that can both read and write the file system, so I think you will have to make the additional methods public.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean public on the DicomAccess, but unimplemented by default.

import fs from "fs";
import path from "path";

export function getDicomFilesFromDirectory(directory) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should use one of the directory tree scanners to get files in that directory and recursively, and unless the glob pattern is *.dcm, it should match everything - there might be an exclusion pattern, to exclude jpg, txt etc.


try {
// Create access instance (currently supports only DICOMweb)
console.log("🔌 Creating DICOM access instance...");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The download action should basically look like:

const access = await DicomAccess.createInstance(url, options);
const destination = await DicomAccess.createInstance(baseDir, options)

const studyAccess = await access.queryStudy()
const studyDest = await access.queryStudy({ create: true })

studyDest.writeStudy(studyAccess);

Then, internally studyAccess should have things iterated from/over that studyDest needs in order to write the study source.

That way, studyDest being a DICOMDIR containing part 10 files can be written to, as can a response to a DICOMweb part 10 query, or a DICOMweb query can all be written to independently of the source type.

this.metadata = metadata;

// Containers to store queried data
this.seriesInstanceUIDsMetadata = new Map();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't assume that everything gets read up front, instead read things as they are requested, grouping them into maps of objects as required with an async function doing the await on each item as it arrives.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants