fo-dicom是如何实现DICOM 的网络通信功能

news2024/9/24 8:29:59

一、前言

前面的文章,我们介绍了fo-dicom是一个怎样的开源库等一些内容:

  • fo-dicom,第一个基于.NET Standard 2.0 开发的DICOM开源库
  • fo-dicom开源库是如何满足 DICOM标准的基本要求
  • fo-dicom开发之DICOM数据解析:常见数据类型及处理方法详解

今天我们分享下 fo-dicom是如何实现DICOM 的网络通信功能。

二、DICOM3.0标准的通用通信模型

下图显示了DICOM3.0标准的通用通信模型,该模型跨越了 网络(在线)和媒体存储交换(离线)通信。应用程序可利用以下任一传输机制:
在这里插入图片描述

  • DICOM 消息服务和上层服务,它们独立于特定的物理网络通信支持和协议(如 TCP/IP)。
  • DICOM Web 服务 API 和 HTTP 服务,允许使用通用超文本和相关协议来传输 DICOM 服务
  • 基本 DICOM 文件服务,它提供独立于特定媒体存储格式和文件结构的存储介质访问
  • DICOM 实时通信,提供基于 SMPTE 和 RTP 的 DICOM 元数据的实时传输。

DICOM的通用通信模型旨在为医疗图像和相关数据的传输和存储提供灵活和多样化的解决方案。DICOM的通用通信模型不仅仅是简单地传输和存储医疗图像和相关数据,而是提供了一种多层次、多种方式的灵活解决方案,以满足不同场景下的需求和要求。这种多样化的传输和存储机制使得DICOM成为医疗行业中不可或缺的通信标准,为医疗图像和相关数据的交换和共享提供了可靠和高效的技术支持。

DICOM的通用通信模型的重要性和价值在于其能够满足医疗行业不同方面的需求,为医疗图像和相关数据的传输和存储提供了全面而可靠的解决方案,从而推动了医疗信息技术的发展和应用。

三、基于.NET 的网络通信功能

fo-dicom 使用了Socket和TcpClient等底层网络通信类来与DICOM服务器进行连接和通信,从而实现 DICOM 的网络通信功能。下面是 fo-dicom 网络通信的实现基本原理:

  1. 传输协议选择fo-dicom 支持多种传输协议,如 TCP/IP、UDP 和 WebSocket。用户可以根据需要选择适合的传输协议。

  2. 连接建立:对于服务器端应用程序,使用 DicomServer 类监听指定的端口号,等待客户端连接请求。一旦有客户端连接请求到达,服务器将建立一个与客户端的网络连接。

  3. 数据传输:在 DICOM 通信中,数据通过 DIMSE(DICOM Message Service Element)进行传输。DIMSE 是基于消息的通信模式,包括 C-STORE(存储服务)、C-FIND(查询服务)、C-MOVE(移动服务)等。

  4. 数据编码fo-dicom 使用 DICOM 标准定义的数据格式和编码规则对数据进行编码。DICOM 数据集使用一系列的标签(Tag)来组织和描述不同的信息,例如患者姓名、图像序列等。fo-dicom 将数据集编码为字节流以进行传输。

  5. 数据解码:在接收方,fo-dicom 将接收到的字节流解码为 DICOM 数据集,以便进行后续的处理和分析。

  6. 数据处理:根据具体的应用需求,可以对 DICOM 数据集进行查询、存储、检索等操作。fo-dicom 提供了一组 API 来处理 DICOM 数据集,以便用户能够方便地访问和操作数据。

  7. 响应发送:在服务器端应用程序中,一旦完成对 DICOM 请求的处理,将向客户端发送一个响应。响应中包含请求的执行结果、状态信息等。

  8. 连接断开:通信完成后,可以关闭服务器端的监听或断开客户端与服务器的连接。

fo-dicom 通过使用 .NET 平台的网络通信库来实现底层的网络传输,并且遵循 DICOM 标准的数据格式和编码规则。它提供了一组简洁而强大的 API,使得用户可以方便地进行 DICOM 数据的传输和处理。

四、案例说明

以下是一个使用 fo-dicom 进行C-STORE命令的网络通信的简单案例:案例来源于官网的示例,https://github.com/fo-dicom/fo-dicom-samples

1. SCP处理C-STORE请求

假设我们有一个服务器端应用程序,它监听在本地的端口号 11112上,等待客户端的连接请求。一旦接收到来自客户端的 C-STORE 请求,服务器将把接收到的 DICOM 图像数据保存到本地磁盘。

using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using FellowOakDicom.Network;
using Microsoft.Extensions.Logging;

namespace Samples
{

    internal class Program
    {

        private const string _storagePath = @".\DICOM";

        private static void Main(string[] args)
        {
            // start DICOM server on port from command line argument or 11112
            var port = args != null && args.Length > 0 && int.TryParse(args[0], out int tmp) ? tmp : 11112;
            Console.WriteLine($"Starting C-Store SCP server on port {port}");

            using (var server = DicomServerFactory.Create<CStoreSCP>(port))
            {
                // end process
                Console.WriteLine("Press <return> to end...");
                Console.ReadLine();
            }
        }


        private class CStoreSCP : DicomService, IDicomServiceProvider, IDicomCStoreProvider, IDicomCEchoProvider
        {
            private static readonly DicomTransferSyntax[] _acceptedTransferSyntaxes = new DicomTransferSyntax[]
            {
               DicomTransferSyntax.ExplicitVRLittleEndian,
               DicomTransferSyntax.ExplicitVRBigEndian,
               DicomTransferSyntax.ImplicitVRLittleEndian
            };

            private static readonly DicomTransferSyntax[] _acceptedImageTransferSyntaxes = new DicomTransferSyntax[]
            {
               // Lossless
               DicomTransferSyntax.JPEGLSLossless,
               DicomTransferSyntax.JPEG2000Lossless,
               DicomTransferSyntax.JPEGProcess14SV1,
               DicomTransferSyntax.JPEGProcess14,
               DicomTransferSyntax.RLELossless,
               // Lossy
               DicomTransferSyntax.JPEGLSNearLossless,
               DicomTransferSyntax.JPEG2000Lossy,
               DicomTransferSyntax.JPEGProcess1,
               DicomTransferSyntax.JPEGProcess2_4,
               // Uncompressed
               DicomTransferSyntax.ExplicitVRLittleEndian,
               DicomTransferSyntax.ExplicitVRBigEndian,
               DicomTransferSyntax.ImplicitVRLittleEndian
            };


            public CStoreSCP(INetworkStream stream, Encoding fallbackEncoding, ILogger log, DicomServiceDependencies dependencies)
                : base(stream, fallbackEncoding, log, dependencies)
            {
            }


            public Task OnReceiveAssociationRequestAsync(DicomAssociation association)
            {
                if (association.CalledAE != "STORESCP")
                {
                    return SendAssociationRejectAsync(
                        DicomRejectResult.Permanent,
                        DicomRejectSource.ServiceUser,
                        DicomRejectReason.CalledAENotRecognized);
                }

                foreach (var pc in association.PresentationContexts)
                {
                    if (pc.AbstractSyntax == DicomUID.Verification)
                    {
                        pc.AcceptTransferSyntaxes(_acceptedTransferSyntaxes);
                    }
                    else if (pc.AbstractSyntax.StorageCategory != DicomStorageCategory.None)
                    {
                        pc.AcceptTransferSyntaxes(_acceptedImageTransferSyntaxes);
                    }
                }

                return SendAssociationAcceptAsync(association);
            }


            public Task OnReceiveAssociationReleaseRequestAsync()
            {
                return SendAssociationReleaseResponseAsync();
            }


            public void OnReceiveAbort(DicomAbortSource source, DicomAbortReason reason)
            {
                /* nothing to do here */
            }


            public void OnConnectionClosed(Exception exception)
            {
                /* nothing to do here */
            }


            public async Task<DicomCStoreResponse> OnCStoreRequestAsync(DicomCStoreRequest request)
            {
                var studyUid = request.Dataset.GetSingleValue<string>(DicomTag.StudyInstanceUID).Trim();
                var instUid = request.SOPInstanceUID.UID;

                var path = Path.GetFullPath(Program._storagePath);
                path = Path.Combine(path, studyUid);

                if (!Directory.Exists(path))
                {
                    Directory.CreateDirectory(path);
                }

                path = Path.Combine(path, instUid) + ".dcm";

                await request.File.SaveAsync(path);

                return new DicomCStoreResponse(request, DicomStatus.Success);
            }


            public Task OnCStoreRequestExceptionAsync(string tempFileName, Exception e)
            {
                // let library handle logging and error response
                return Task.CompletedTask;
            }


            public Task<DicomCEchoResponse> OnCEchoRequestAsync(DicomCEchoRequest request)
            {
                return Task.FromResult(new DicomCEchoResponse(request, DicomStatus.Success));
            }

        }
    }
}

在上面的代码中,首先从命令行参数中获取端口号,然后创建一个 CStoreSCP 对象作为 DICOM 服务器,并将其绑定到指定的端口。在 CStoreSCP 类中,实现了 IDicomServiceProvider、IDicomCStoreProvider 和 IDicomCEchoProvider 接口,分别处理 DICOM 关联请求、C-Store 请求和 C-Echo 请求。其中,
OnReceiveAssociationRequestAsync() 方法会检查 Called AE 是否为 STORESCP,如果不是则拒绝关联请求。OnCStoreRequestAsync() 方法则会将接收到的 DICOM 数据保存到本地文件系统中。其他的方法实现通常为空实现,因为并不需要对其进行特殊处理。

2. SCU发送C-STORE请求

对于客户端应用程序,我们可以使用 DicomClient 类来发送 C-STORE 请求到服务器。以下是一个简单的客户端示例:

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using FellowOakDicom.Network;
using FellowOakDicom.Network.Client;

namespace Samples
{
    internal static class Program
    {

        private static string _storeServerHost = "127.0.0.1";
        private static int _storeServerPort = 11112;
        private const string _storeServerAET = "STORESCP";
        private const string _aet = "FODICOMSCU";

        static async Task Main(string[] args)
        {
            var storeMore = "";

            _storeServerHost = GetServerHost();
            _storeServerPort = GetServerPort();

            Console.WriteLine("***************************************************");
            Console.WriteLine("Server AE Title: " + _storeServerAET);
            Console.WriteLine("Server Host Address: " + _storeServerHost);
            Console.WriteLine("Server Port: " + _storeServerPort);
            Console.WriteLine("Client AE Title: " + _aet);
            Console.WriteLine("***************************************************");

            var client = DicomClientFactory.Create(_storeServerHost, _storeServerPort, false, _aet, _storeServerAET);
            client.NegotiateAsyncOps();

            do
            {
                try
                {
                    Console.WriteLine();
                    Console.WriteLine("Enter the path for a DICOM file:");
                    Console.Write(">>>");
                    string dicomFile = Console.ReadLine();

                    while (!File.Exists(dicomFile))
                    {
                        Console.WriteLine("Invalid file path, enter the path for a DICOM file or press Enter to Exit:");

                        dicomFile = Console.ReadLine();

                        if (string.IsNullOrWhiteSpace(dicomFile))
                        {
                            return;
                        }
                    }

                    var request = new DicomCStoreRequest(dicomFile);

                    request.OnResponseReceived += (req, response) => Console.WriteLine("C-Store Response Received, Status: " + response.Status);

                    await client.AddRequestAsync(request);
                    await client.SendAsync();
                }
                catch (Exception exception)
                {
                    Console.WriteLine();
                    Console.WriteLine("----------------------------------------------------");
                    Console.WriteLine("Error storing file. Exception Details:");
                    Console.WriteLine(exception.ToString());
                    Console.WriteLine("----------------------------------------------------");
                    Console.WriteLine();
                }

                Console.WriteLine("To store another file, enter \"y\"; Othersie, press enter to exit: ");
                Console.Write(">>>");
                storeMore = Console.ReadLine().Trim();

            } while (storeMore.Length > 0 && storeMore.ToLower()[0] == 'y');
        }

        private static string GetServerHost()
        {
            var hostAddress = "";
            var localIP = GetLocalIPAddress();
            do
            {
                Console.WriteLine("Your local IP is: " + localIP);
                Console.WriteLine("Enter \"1\" to use your local IP Address: " + localIP);
                Console.WriteLine("Enter \"2\" to use defult: " + _storeServerHost);
                Console.WriteLine("Enter \"3\" to enter custom");
                Console.Write(">>>");

                string input = Console.ReadLine().Trim().ToLower();

                if (input.Length > 0)
                {
                    if (input[0] == '1')
                    {
                        hostAddress = localIP;
                    }
                    else if (input[0] == '2')
                    {
                        hostAddress = _storeServerHost;
                    }
                    else if (input[0] == '3')
                    {
                        Console.WriteLine("Enter Server Host Address:");
                        Console.Write(">>>");

                        hostAddress = Console.ReadLine();
                    }
                }
            } while (hostAddress.Length == 0);


            return hostAddress;
        }

        private static int GetServerPort()
        {

            Console.WriteLine("Enter Server port, or \"Enter\" for default \"" + _storeServerPort + "\":");
            Console.Write(">>>");

            var input = Console.ReadLine().Trim();

            return string.IsNullOrEmpty(input) ? _storeServerPort : int.Parse(input);
        }

        public static string GetLocalIPAddress()
        {
            var host = Dns.GetHostEntry(Dns.GetHostName());

            foreach (var ip in host.AddressList)
            {
                if (ip.AddressFamily == AddressFamily.InterNetwork)
                {
                    return ip.ToString();
                }
            }

            return "";
        }
    }
}

首先定义了一些变量,包括存储服务器的主机地址、端口号,以及客户端和服务器的 AE(Application Entity)标题。然后,在 Main 方法中创建了一个 DicomClient 对象,并通过调用 NegotiateAsyncOps 方法进行异步操作的协商。接下来,进入一个循环,用户可以输入要发送的 DICOM 文件的路径。程序会检查路径是否有效,如果无效则提示用户重新输入,直到输入为空或用户选择退出。然后,创建一个 DicomCStoreRequest 对象,传入要发送的 DICOM 文件路径作为参数。并通过订阅 OnResponseReceived 事件来处理响应。最后,调用 AddRequestAsync 方法将请求添加到客户端的请求队列中,并调用 SendAsync 方法发送请求。

其中GetServerHost 方法用于获取服务器主机地址,它会提示用户选择使用本地 IP 地址、默认地址还是自定义地址。GetServerPort 方法用于获取服务器端口号,用户可以输入自定义端口号,或者直接回车使用默认端口号。GetLocalIPAddress 方法用于获取本地 IP 地址。

3. 小结

这个案例展示了一个简单的基于 fo-dicom 的 DICOM 网络通信示例,即SCU和SCP对CStore的通信的简单处理,服务器接收到客户端发送的 C-STORE 请求并保存图像到本地磁盘。

总结

关于C-STORE的处理,如果不是特别清楚,后续,我们在一起学习关于通信协议相关的内容,重点分析,C-ECHO,C-Store,C-Find,C-Move等等,加深理解和使用场景。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2159900.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

自学前端的正确姿势是...

师傅带进门&#xff0c;修行在个人。 在前端自学成才的道路上&#xff0c;有些人走的很快&#xff0c;有些人却举步维艰。 为什么会这样子呢&#xff1f;因为他们没有掌握自学前端的正确姿势。 在介绍应该要怎样自学前端之前&#xff0c;首先来看下&#xff0c;自学前端容易…

JavaWeb--小白笔记07:servlet对表单数据的简单处理

这里的servlet对表单数据的处理是指使用IDEA创建web工程&#xff0c;再创建html和class文件进行连接&#xff0c;实现html创建一个表单网页&#xff0c;我们对网页中的表单进行填充&#xff0c;可以通过class文件得到网页我们填充的内容进行打印到控制台。 一登录系统页面---h…

[vulnhub] w1r3s.v1.0

https://www.vulnhub.com/entry/w1r3s-101,220/ 思路&#xff1a;红队笔记 主机发现端口扫描 使用nmap扫描网段类存活主机 因为靶机是我最后添加的&#xff0c;所以靶机IP是133 nmap -sP 192.168.75.0/24 // Starting Nmap 7.93 ( https://nmap.org ) at 2024-09-20 09:09 CST…

MySQL 数据库安装(详细教程)

文章目录 一、前言二、下载 MySQL2.1 安装包方式2.2 压缩包方式&#xff08;推荐&#xff09; 三、安装 MySQL3.1 解压 MySQL 文件3.2 配置环境变量3.3 初始化 data 目录3.4 安装 MySQL 服务3.5 开启 MySQL 服务3.6 修改 MySQL 密码 四、卸载 MySQL4.1 停止 MySQL 服务4.2 删除…

MySQL record 08 part

数据库连接池&#xff1a; Java DataBase Connectivity&#xff08;Java语言连接数据库&#xff09; 答&#xff1a; 使用连接池能解决此问题&#xff0c; 连接池&#xff0c;自动分配连接对象&#xff0c;并对闲置的连接进行回收。 常用的数据库连接池&#xff1a; 建立数…

【WRF运行第三期】服务器上运行WRF模型(官网案例-Hurricane Matthew)

【WRF运行第三期】运行WRF模型&#xff08;官网案例-Hurricane Matthew&#xff09; 官网案例-Hurricane Matthew介绍0 创建DATA文件夹1 WPS预处理1.1 解压GRIB数据&#xff08;ungrib.exe&#xff09;1.1.1 解压GRIB数据---GFS&#xff08;Matthew案例研究数据&#xff09;1.1…

Bytebase 2.22.3 - 一键回滚 PostgreSQL DML 变更

&#x1f680; 新功能 支持一键回滚 PostgreSQL DML 变更。 &#x1f384; 改进 优化 DML 事前备份和回滚体验&#xff1a; 引导用户创建 bbdataarchive 数据库。如果没有 bbdataarchive 数据库&#xff0c;无法开启备份功。用户现在可以在创建工单之后开启或关闭备份功能&a…

PyCharm远程连接AutoDL服务器实现程序调试

本文详细介绍了如何在Pycharm中配置SSH和SFTP&#xff0c;以便于在AOTUDL服务器上进行代码修改、调试。步骤包括新建工程、配置SFTP连接、设置Rootpath和Mapping&#xff0c;以及实现自动上传和下载文件的功能。远程服务器编辑调试只是试用于专业版本的pycharm&#xff0c;我的…

【LLM学习之路】9月22日 第九天 自然语言处理

【LLM学习之路】9月22日 第九天 直接看Transformer 第一章 自然语言处理 自然语言处理发展史 只要看的足够多&#xff0c;未必需要理解语言 统计语言模型发展史 统计语言模型&#xff1a; 判断一个句子是否合理&#xff0c;就计算这个句子会出现的概率 缺点是句子越长越…

微软推迟在MDM设备上启用OOBE强制更新 因为IT管理员反馈称缺乏控制

微软很久之前就计划在 Windows 10/11 OOBE 期间强制下载更新&#xff0c;即若检测到系统本身属于旧版本例如并未安装最新累积更新&#xff0c;则在 OOBE 期间强制下载最新累积更新并自动安装。这种更新方式已经在面向消费者的设备上启用&#xff0c;而上周微软则是在适用于企业…

盘点那些功能强大的思维导图在线工具,你用过几个

如果我们日常遇到比较繁杂的信息需要梳理&#xff0c;那我比较推荐使用思维导图在线工具进行梳理。这些工具可以通过图形化的方式展示各种信息之间的关系。这篇文章我将要介绍几款好用的思维导图工具帮我们更好的组织思维。 1.福晰思维导图 链接一下&#xff1a;https://www.…

GPIO与MIO控制LED——ZYNQ学习笔记2

一、GPIO简介 ZYNQ 分为 PS 和 PL 两部分&#xff0c;那么器件的引脚&#xff08; Pin&#xff09;资源同样也分成了两部分。 ZYNQ PS 中的外设可以通过 MIO&#xff08; multiplexed I/O&#xff0c;多路复用 I/O&#xff09;模块连接到 PS 端的引脚上&#xff0c;也可以通过 …

HTML讲解(三)通用部分

目录 1.空格标记 2.特殊文字的标记 3.注释语句 4.对文字字体的设置 5.修改文字形态 6.换行标记 7.居中标记 8.水平线标记 9.设置滚动弹幕 1.空格标记 在HTML中&#xff0c;我们想打印空格并不能直接敲一个空格键&#xff0c;因为如果是敲空格键&#xff0c;那无论你敲…

【JUC并发编程系列】深入理解Java并发机制:Volatile从底层原理解析到高级应用技巧(六、Volatile关键字、JMM、重排序、双重检验锁)

文章目录 【JUC并发编程系列】深入理解Java并发机制&#xff1a;Volatile从底层原理解析到高级应用技巧(六、Volatile关键字、JMM、重排序、双重检验锁)1. Volatile的特性2. Volatile的用法3. CPU多核硬件架构剖析4. JMM内存模型4.1 主要特性4.2 JMM 的工作原理4.3 实现机制 5.…

Leetcode面试经典150题-39.组合总数进阶:40.组合总和II

本题是扩展题&#xff0c;真实考过&#xff0c;看这个题之前先看一下39题 Leetcode面试经典150题-39.组合总数-CSDN博客 给定一个候选人编号的集合 candidates 和一个目标数 target &#xff0c;找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数…

Docker:解决开发运维问题的开源容器化平台

云计算de小白 Docker是一个开源的容器化平台&#xff0c;可以将应用程序及其依赖的环境打包成轻量级、可移植的容器。 Docker为什么这么受欢迎呢?原因很简单&#xff1a;Docker可以解决不同环境一致运行的问题&#xff0c;而且占用资源少&#xff0c;速度快。 所以好的东西…

链式队列操作

文章目录 &#x1f34a;自我介绍&#x1f34a;概述&#x1f34a;链式队列代码linkstack.clinkstack.hmain.c 你的点赞评论就是对博主最大的鼓励 当然喜欢的小伙伴可以&#xff1a;点赞关注评论收藏&#xff08;一键四连&#xff09;哦~ &#x1f34a;自我介绍 Hello,大家好&…

OmniPeek 空口抓包软件安装指导

OmniPeek 空口抓包软件安装指导 1 双击omnp75安装包---Unzip解压缩 生成install包 2 进入install文件夹点击setup开始进入安装界面 3 点击install Omnipeek 4 点击next,勾选手动安装

云原生虚拟化kubevirt安装

kubevirt 介绍 Kubevirt 是 Redhat 开源的一套以容器方式运行虚拟机的项目&#xff0c;通过 kubernetes 云原生方式来管理虚拟机生命周期。它通过使用自定义资源&#xff08;CRD&#xff09;和其它 Kubernetes 功能来无缝扩展现有的集群&#xff0c;以提供一组可用于管理虚拟机…

9.23作业

仿照string类&#xff0c;自己手动实现 My_string 代码如下 MyString.h #ifndef MYSTRING_H #define MYSTRING_H #include <iostream> #include <cstring>using namespace std;class My_string { private:char *ptr; //指向字符数组的指针int size; …