Medical imaging relies on DICOM (ISO 12052) — the universal standard implemented in hundreds of thousands of imaging devices worldwide. Robotics relies on ROS 2. Until now, engineers bridging these two worlds had to manually convert DICOM files to PNGs, JPEGs, or point clouds before running their ROS pipelines, losing spatial metadata and scaling information in the process.
dicom_to_ros eliminates that bottleneck. It is a fully distributed, microservice-based ROS 2 pipeline that receives DICOM files over the network and translates them in real-time into standard ROS 2 topics — images, point clouds, video streams, coordinate transforms, and study metadata — with zero manual pre-processing.
This framework is designed to accelerate innovation in robotic-assisted surgery, medical computer vision, and any research domain where clinical imaging data needs to meet a ROS 2 pipeline.
- Real-time DICOM ingestion via standard C-STORE SCP network protocol (no file system polling)
- 2D slice publishing as
sensor_msgs/Image+sensor_msgs/CameraInfowith correct pixel spacing - 3D volume streaming as a live ROS 2 video feed (
sensor_msgs/Imagesequence) - Volumetric point cloud generation from multi-frame DICOM with physically accurate spacing
- Patient coordinate system → ROS TF tree mapping, converting DICOM directional cosines to a quaternion transform
- Study metadata republished as structured
StudyInfomessages (patient demographics, modality, date, etc.) - Timestamp synchronization: all messages from a single DICOM file share the same
header.stamp, enabling exactmessage_filters::TimeSynchronizeralignment - Fully containerized: plug-and-play Docker Compose setup with RViz2 visualization included
dicom_to_ros/
├── docker/ # Core pipeline Docker setup
│ ├── Dockerfile # ROS 2 pipeline image (listener + all nodes)
│ └── docker-compose.yml # Runs the dicom_listener container
├── dicom_interfaces/ # Custom ROS 2 message definitions
│ └── msg/
│ ├── Dicom.msg # Central internal message (metadata + pixel data)
│ └── StudyInfo.msg # Patient and study metadata subset
├── dicom_to_ros/ # Core ROS 2 package — 6 microservice nodes
│ ├── dicom_to_ros/
│ │ ├── dicom_server.py # DICOM SCP listener (entry point)
│ │ ├── dicom_2_img.py # 2D image publisher
│ │ ├── dicom_2_video.py # 3D volume → video stream publisher
│ │ ├── dicom_2_pcl.py # Point cloud publisher
│ │ ├── dicom_2_tf.py # TF transform publisher
│ │ ├── dicom_2_study_info.py # Study metadata publisher
│ │ └── dicom_utils.py # Shared utilities
│ ├── launch/
│ │ └── dicom_nodes.launch.py
│ └── test/ # Unit tests per node
├── dicom_to_ros_demo/ # Demo environment
│ ├── dicom_samples/ # DICOM test data (auto-downloaded on first run)
│ │ ├── 2D/
│ │ │ ├── CT/
│ │ │ └── MRI/
│ │ └── 3D/
│ │ ├── MRI/
│ │ └── US/
│ ├── docker/ # Demo-specific Docker setup
│ │ ├── Dockerfile.downloader # Sample downloader image
│ │ ├── Dockerfile.rviz # RViz2 visualization image
│ │ └── docker-compose.yml # Orchestrates downloader + RViz2
│ ├── rviz_config/ # Pre-configured RViz2 layout
│ │ └── default.rviz
│ └── test_data_utils/
│ └── download_samples.py # Script to fetch public DICOM samples
└── doc/
├── dicom_to_ros.png
└── video_preview.png
The pipeline uses a fan-out microservice architecture. A single dicom_server node acts as the DICOM network listener and publishes a comprehensive internal Dicom message. All downstream nodes subscribe independently to that topic and produce their specialized ROS 2 output.
DICOM Client (storescu)
│ C-STORE (port 11112)
▼
┌───────────────┐
│ dicom_server │──── /dicom_interfaces/Dicom ────┬────────────────────┬──────────────────┬──────────────────┬──────────────────┐
└───────────────┘ │ │ │ │ │
┌───────────────┐ ┌────────────────┐ ┌──────────────┐ ┌────────────────┐ ┌──────────────┐
│dicom2studyinfo│ │ dicom2img │ │ dicom2video │ │ dicom2pcl │ │ dicom2tf │
└───────┬───────┘ └───────┬────────┘ └──────┬───────┘ └───────┬────────┘ └──────┬───────┘
│ │ │ │ │
/dicom_study_info /dicom_image /dicom_video_frames /dicom_point_cloud /tf
/dicom_camera_info /dicom_video_camera_info
| Node | Description |
|---|---|
dicom_server |
DICOM Storage SCP. Receives C-STORE requests, parses the file, and publishes the central dicom_interfaces/Dicom message. |
dicom2studyinfo |
Extracts patient demographics and study metadata; republishes as dicom_interfaces/StudyInfo. |
dicom2img |
Handles single-frame (2D) scans. Publishes a normalized grayscale sensor_msgs/Image and sensor_msgs/CameraInfo. |
dicom2video |
Handles multi-frame (3D) volumes. Streams slices as a sensor_msgs/Image sequence alongside sensor_msgs/CameraInfo. |
dicom2pcl |
Generates a sensor_msgs/PointCloud2 from volumetric data using pixel spacing, slice thickness, and intensity thresholding. |
dicom2tf |
Reads Image Position/Orientation (Patient) DICOM tags. Converts directional cosines to a quaternion and broadcasts the patient_frame → dicom_optical_frame transform via /tf. |
| Topic | Type | Publisher | Description |
|---|---|---|---|
/dicom_interfaces/Dicom |
dicom_interfaces/Dicom |
dicom_server |
Central internal message: parsed metadata + raw pixel data. |
/dicom_study_info |
dicom_interfaces/StudyInfo |
dicom2studyinfo |
Patient ID, name, modality, date, series description. |
/dicom_image |
sensor_msgs/Image |
dicom2img |
2D image normalized to 8-bit grayscale. |
/dicom_camera_info |
sensor_msgs/CameraInfo |
dicom2img |
Camera intrinsics for /dicom_image. |
/dicom_video_frames |
sensor_msgs/Image |
dicom2video |
Per-slice video stream from a 3D volume. |
/dicom_video_camera_info |
sensor_msgs/CameraInfo |
dicom2video |
Camera intrinsics for /dicom_video_frames. |
/dicom_point_cloud |
sensor_msgs/PointCloud2 |
dicom2pcl |
3D point cloud with intensity values from volumetric data. |
/tf |
tf2_msgs/TFMessage |
dicom2tf |
Patient coordinate system → image frame transform. |
Synchronization: All messages produced from a single DICOM file share the same
header.stamp, making them compatible withmessage_filters::TimeSynchronizerfor exact alignment of spatial, visual, and clinical data.
DICOM images often come in 12-bit or 16-bit integers with varying ranges.
To make them compatible with standard Computer Vision tools (OpenCV/ROS), the imaging nodes perform Min-Max normalization to cast them to mono8 (uint8):
The dicom_server node is responsible for parsing the incoming DICOM file. It extracts all necessary metadata—including patient info, study details, and geometric data—and publishes it in a single, comprehensive dicom_interfaces/Dicom message.
Downstream nodes subscribe to this topic and use the pre-parsed data:
- The
dicom2studyinfonode subscribes to theDicommessage and republishes a subset of this information (patient demographics and study details) as aStudyInfomessage. The fields include:- Identifiers:
patient_id,patient_name,sop_instance_uid - Demographics:
sex,age - Scan Details:
modality,study_date,series_description
- Identifiers:
- The imaging (
dicom2img,dicom2video) and point cloud (dicom2pcl) nodes use the geometric data likepixel_spacingandslice_thicknessdirectly from theDicommessage to generate physically accurateCameraInfoandPointCloud2messages.
- Docker and Docker Compose
- A Linux host with X11 (for RViz2 visualization)
The docker/ directory runs the ROS 2 DICOM listener and all processing nodes. It mounts the dicom_interfaces and dicom_to_ros packages from the repo root into the container workspace.
git clone https://github.com/Ekumen-OS/dicom_to_ros.git
cd dicom_to_ros
export UID=$(id -u)
export GID=$(id -g)
docker compose -f docker/docker-compose.yml up --build -dThe pipeline starts immediately and listens for DICOM C-STORE requests on port 11112.
| Parameter | Default | Description |
|---|---|---|
ae_title |
ROS_DICOM_AE |
DICOM Application Entity Title |
port |
11112 |
TCP port for incoming C-STORE connections |
This project supports ROS 2 Security (SROS2) to protect sensitive patient data transmitted over the network. When enabled, SROS2 provides authentication, encryption, and access control for all internal topics, including the raw /dicom_interfaces/Dicom message and the derived /dicom_study_info topic.
Security is enabled by default in the provided docker-compose.yml file via environment variables. On the first run, a script generates a security keystore containing the necessary keys and certificates for all pipeline nodes. This keystore is persisted in a Docker volume named keystore.
Note: The initial key generation is a one-time process. If the container is stopped during this first run, the security volume may be left in a corrupted state. If this happens, you will need to manually remove the Docker volume (
docker volume rm <project>_keystore) before restarting the container.
By default, only the internal dicom_to_ros nodes can communicate. To grant your own ROS 2 node access to a secured topic (e.g., /dicom_study_info):
-
Add a Profile to the Policy: Open
dicom_to_ros/dicom_to_ros/security/permissions.xmland add a new<profile>for your node inside the<profiles>block. You must grant it permissions to the system topics (/rosout,/parameter_events) and services (~/*) in addition to any data topics.Example for a node named
my_subscriberthat needs to read study info:<profile ns="/dicom_to_ros" node="my_subscriber"> <topics subscribe="ALLOW"> <topic>/dicom_to_ros/dicom_study_info</topic> <topic>/parameter_events</topic> </topics> <topics publish="ALLOW"> <topic>/rosout</topic> <topic>/parameter_events</topic> </topics> <services reply="ALLOW"> <service>~/*</service> </services> </profile>
-
Regenerate the Keystore: The security artifacts must be regenerated to include your new node. Bring down the pipeline and remove the old keystore volume.
docker compose -f docker/docker-compose.yml down -v
-
Restart the Pipeline: The entrypoint script will automatically generate a new keystore that includes keys and permissions for your node.
docker compose -f docker/docker-compose.yml up -d
-
Launch Your Node: When you launch your application, ensure it joins the correct namespace and enclave.
# Example in a Python launch file Node( package='my_package', executable='my_subscriber_node', name='my_subscriber', namespace='/dicom_to_ros', ros_arguments=['--enclave', '/dicom_to_ros'] )
For a more detailed demo, please check the dicom_to_ros_demo folder.
Contributions are welcome. Please read CONTRIBUTING.md for the full workflow, code style guidelines, and commit message conventions.
For bugs and feature requests, open an issue on GitHub. Security vulnerabilities should be reported privately to security@ekumenlabs.com.
This project is licensed under the Apache License 2.0. See LICENSE for details.

