首页 > 其他分享 >DICOM 文件传输 基于DCMTK

DICOM 文件传输 基于DCMTK

时间:2023-09-28 14:14:35浏览次数:42  
标签:DICOM MOVE 文件传输 发送 DCMTK SCU OFString SCP STORE

DcmSCP:dicom service class provider,相当于服务器

DcmSCU:dicom service class user,相当于客户端

DIMSE:dicom message service element,dicom连接中传递的消息单元

 

一、建立连接

DICOM网络连接建立在TCP基础上,使用IP地址和端口号通信。

1. SCP开始监听端口

2. 初始化TCP连接

3. SCU向SCP发送连接请求

4. SCP接收连接请求消息,查找是否有支持的服务

5. 若有支持的服务,SCP向SCU发送连接确认消息,SCU收到确认消息后DICOM连接建立。

6. 否则SCP向SCU发送连接拒绝消息,断开TCP连接

 

二、消息类型

DIMSE有C-Style风格和N-Style风格两种,PACS系统之间传输文件一般使用C-Style消息。

1. C-ECHO 用于确认连接是否建立

2. C-STORE 用于发送文件并存储

3. C-MOVE 用于查询和移动文件

4. C-GET 用于查询和拉取文件

5. C-FIND 用于查询文件

每种消息都有请求和确认两种。部分服务流程如下:

·C-STORE

SCU向SCP发送请求消息,消息中带有待存储的dicom数据文件,SCP收到消息后将数据文件存储在服务器,然后向SCU返回确认消息,包含处理结果。

·C-MOVE

  SCU向SCP发送请求消息,消息中带有查询数据信息和移动目标的AETitle,SCP收到消息后,从服务器文件中查询是否有符合条件的文件,如果有,另外创建一个SCU,通过该SCU向目标发送C-STORE请求,等待C-STORE回应。一次C-MOVE操作中可能会包含多次C-STORE子操作。待所有符合条件的dicom文件都发送完毕后,关闭其创建的SCU,释放连接,然后向最初发送C-MOVE请求的SCU返回C-MOVE确认,包含C-MOVE的处理结果。(实际上对于每次C-STORE子操作都应当返回一次C-MOVE确认消息,但编写程序时也可只在最后返回确认消息,这取决于你的实际需求。)

三、基于DCMTK的示例

·头文件

 

#pragma once
#include "dcmtk/config/osconfig.h"
#include "dcmtk/dcmnet/scp.h"
#include "dcmtk/dcmnet/scu.h"
#include "dcmtk/dcmnet/diutil.h"
#include "dcmtk/ofstd/offname.h" //OFFilenameCreator 类

class DataItem :public DcmDataset
{
public:
    //构造函数获取dataset
    DataItem(DcmDataset& old) :DcmDataset(old) {};
    ~DataItem() {};
    DcmList* getElementList() const
    {
        return this->elementList;
    }
};

class SCP :public DcmSCP
{
public:
    SCP();
    SCP(const SCP& old);
    ~SCP();
    
    void initSCU();
    void setOutputDirectory(OFString path);
    // 处理收到的命令,C-ECHO、C-STORE、C-GET、C-MOVE等
    virtual OFCondition handleIncomingCommand(T_DIMSE_Message* incomingMsg, const DcmPresentationContextInfo& presInfo);
    OFCondition generateSTORERequestFilename(const T_DIMSE_C_StoreRQ& reqMessage, OFString& filename, OFString studyInstanceUID);
    OFCondition generateDirAndFilename(OFString& filename, OFString& directoryName, OFString& sopClassUID, OFString& sopInstanceUID, OFString studyInstanceUID);
    // 处理C-MOVE服务
    OFCondition handleMOVE(DcmDataset* dataset, OFString dest);
    OFCondition queryFilewithDataset(OFList<OFString>& files, DcmDataset dataset);
    OFCondition ConnectToDest();
    //该函数控制退出listen循环,只需重载,会在listen函数中被调用。
    virtual OFBool stopAfterCurrentAssociation();
    void setIsTmp(bool stat);

private:
    OFString OutputDirectory = "D:/DICOMSTORE";
    OFString QueryDirectory = "D:/DICOMSTORE/1.2.276.0.7230010.3.1.4.3707881089.6120.1625463501.901";
    DcmSCU scu;
    OFString moveDest;
    bool isTmp = false;
};

void getFiles(OFString path, OFList<OFString>& files);

 

·源文件

#include "SCP.h"


SCP::SCP()
{
    
}

SCP::~SCP()
{

}

void SCP::initSCU()
{
    scu.setPeerAETitle(moveDest);
    scu.setPeerPort(11114);
    scu.setPeerHostName("127.0.0.1");
    setVerbosePCMode(OFTrue);
    OFList<OFString> ts;
    ts.push_back(UID_LittleEndianExplicitTransferSyntax);
    ts.push_back(UID_BigEndianExplicitTransferSyntax);
    ts.push_back(UID_LittleEndianImplicitTransferSyntax);
    scu.addPresentationContext(UID_CTImageStorage, ts);
    scu.addPresentationContext(UID_SecondaryCaptureImageStorage, ts);
    scu.addPresentationContext(UID_VerificationSOPClass, ts);// 响应C-ECHO

}

OFCondition SCP::handleIncomingCommand(T_DIMSE_Message* incomingMsg, const DcmPresentationContextInfo& presInfo)
{
    //该函数尚未接收来自scu的dataset,只接收了命令信息
    OFCondition cond;
    OFCondition status = EC_IllegalParameter;
    // 处理 C-ECHO 请求
    if ((incomingMsg->CommandField == DIMSE_C_ECHO_RQ) && (presInfo.abstractSyntax == UID_VerificationSOPClass))
    {
        DCMNET_DEBUG("C-ECHO");
        cond = handleECHORequest(incomingMsg->msg.CEchoRQ, presInfo.presentationContextID);
    }
    else if ((incomingMsg->CommandField == DIMSE_C_STORE_RQ))
    {
        // 处理 C-STORE 请求
        DCMNET_DEBUG("C-STORE");
        // 接收数据
        T_DIMSE_C_StoreRQ& storeReq = incomingMsg->msg.CStoreRQ;
        Uint16 rspStatusCode = STATUS_STORE_Error_CannotUnderstand;

        DcmFileFormat fileformat;
        DcmDataset* reqDataset= fileformat.getDataset();
        status = receiveSTORERequest(storeReq, presInfo.presentationContextID, reqDataset);
        OFString studyInstanceUID;
        reqDataset->findAndGetOFString(DCM_StudyInstanceUID, studyInstanceUID);
        // 直接保存为文件
        OFString filename;
        // 生成文件名(包含目录)
        status = generateSTORERequestFilename(storeReq, filename, studyInstanceUID);
        if (status.good())
        {
            if (OFStandard::fileExists(filename))
                DCMNET_WARN("file already exists, overwriting: " << filename);

            // 调用 receiveSTORERequest 函数接收并保存 dataset 为文件
            //status = receiveSTORERequest(storeReq, presInfo.presentationContextID, filename);
            status = fileformat.saveFile(filename);
            if (status.good())
            {
                rspStatusCode = STATUS_Success;
            }
        }
        // 发送回应消息
        if (status.good())
            status = sendSTOREResponse(presInfo.presentationContextID, storeReq, rspStatusCode);
        else if (status == DIMSE_OUTOFRESOURCES)
        {
            sendSTOREResponse(presInfo.presentationContextID, storeReq, STATUS_STORE_Refused_OutOfResources);
        }
    }
    else if ((incomingMsg->CommandField == DIMSE_C_MOVE_RQ))
    {
        // 处理 C-MOVE 请求
        /*接收C-MOVE消息
        * 服务器数据查询符合条件的文件
        * 向指定sop发送C-STORE,发送符合条件的文件
        * 收到C-STORE回应
        * 发送C-MOVE回应
        */
        DCMNET_DEBUG("C-MOVE");
        DcmFileFormat fileformat;
        DcmDataset* reqDataset = fileformat.getDataset();

        T_DIMSE_C_MoveRQ& moveReq = incomingMsg->msg.CMoveRQ;
        Uint16 rspStatusCode = STATUS_MOVE_Failed_UnableToProcess;

        //接收查询条件和移动目的地,moveDest为sop目标的aetitle
        status = receiveMOVERequest(moveReq, presInfo.presentationContextID, reqDataset, moveDest);
        if (status.good())
        {
            if (moveDest.empty())
            {
                //目标AEtitle为空
                sendMOVEResponse(presInfo.presentationContextID, moveReq.MessageID,
                    moveReq.AffectedSOPClassUID, NULL, STATUS_MOVE_Failed_MoveDestinationUnknown);
            }
            //处理C-MOVE请求
            status = handleMOVE(reqDataset, moveDest);
            if (status.bad())
            {
                //发送处理成功信息
                //有失败的子操作
                sendMOVEResponse(presInfo.presentationContextID, moveReq.MessageID,
                    moveReq.AffectedSOPClassUID, reqDataset, STATUS_MOVE_Warning_SubOperationsCompleteOneOrMoreFailures);
            }            
            //操作成功时不应返回任何dataset
            sendMOVEResponse(presInfo.presentationContextID, moveReq.MessageID, moveReq.AffectedSOPClassUID, NULL, STATUS_Success);
        }
    }
    else
    {
        // 其他请求全部拒绝        
        OFString tempStr;
        DCMNET_ERROR("Cannot handle this kind of DIMSE command (0x"
            << STD_NAMESPACE hex << STD_NAMESPACE setfill('0') << STD_NAMESPACE setw(4)
            << OFstatic_cast(unsigned int, incomingMsg->CommandField) << ")");
        DCMNET_DEBUG(DIMSE_dumpMessage(tempStr, *incomingMsg, DIMSE_INCOMING));
        cond = DIMSE_BADCOMMANDTYPE;
    }
    return cond;
}

OFCondition SCP::generateSTORERequestFilename(const T_DIMSE_C_StoreRQ& reqMessage, OFString& filename, OFString studyInstanceUID)
{
    OFString directoryName;
    OFString sopClassUID = reqMessage.AffectedSOPClassUID;
    OFString sopInstanceUID = reqMessage.AffectedSOPInstanceUID;
    // 生成文件名
    OFCondition status = generateDirAndFilename(filename, directoryName, sopClassUID, sopInstanceUID, studyInstanceUID);
    if (status.good())
    {
        DCMNET_DEBUG("generated filename for object to be received: " << filename);
        // 创建存储目录
        status = OFStandard::createDirectory(directoryName, OutputDirectory /* rootDir */);
        if (status.bad())
            DCMNET_ERROR("cannot create directory for object to be received: " << directoryName << ": " << status.text());
    }
    else
        DCMNET_ERROR("cannot generate directory or file name for object to be received: " << status.text());
    return status;
}

OFCondition SCP::generateDirAndFilename(OFString& filename, OFString& directoryName, OFString& sopClassUID, OFString& sopInstanceUID, OFString studyInstanceUID)
{
    OFCondition status = EC_Normal;    
    
    // 生成目录名
    OFString generatedDirName;
    if (!studyInstanceUID.empty())
    {
        OFOStringStream stream;
        stream << studyInstanceUID<< OFStringStream_ends;
        OFSTRINGSTREAM_GETSTR(stream, tmpString)
            generatedDirName = tmpString;
        OFSTRINGSTREAM_FREESTR(tmpString);
    }

    // 连接文件路径
    OFStandard::combineDirAndFilename(directoryName, OutputDirectory, generatedDirName);
    // 生成文件名
    OFString generatedFileName;
    if (sopClassUID.empty())
        status = NET_EC_InvalidSOPClassUID;
    else if (sopInstanceUID.empty())
        status = NET_EC_InvalidSOPInstanceUID;
    else
    {
        OFOStringStream stream;
        stream << dcmSOPClassUIDToModality(sopClassUID.c_str(), "UNKNOWN")
            << '.' << sopInstanceUID << ".dcm" << OFStringStream_ends;
        OFSTRINGSTREAM_GETSTR(stream, tmpString)
            generatedFileName = tmpString;
        OFSTRINGSTREAM_FREESTR(tmpString);
        // 连接文件路径和文件名
        OFStandard::combineDirAndFilename(filename, directoryName, generatedFileName);
    }
    return status;
}

OFCondition SCP::handleMOVE(DcmDataset* dataset, OFString dest)
{

    OFList<OFString> files;
    queryFilewithDataset(files, *dataset);
    OFCondition result = ConnectToDest();
    if (result.bad())
    {
        return result;
    }
    if (files.empty())
    {
        result = scu.sendECHORequest(0);//建立一次连接,用于关闭tmpSCP监听
        return EC_Normal;
    }
    else
    {
        

        Uint16 rsp;
        for (auto file : files)
        {
            //逐个发送文件到dest目的
            //需要先初始化scu与目标sop的连接
            result = scu.sendSTORERequest(0, file, NULL, rsp);
            if (result.bad())
            {
                DCMNET_ERROR(result.text());
                //sendMOVEResponse();
            }
        }
        scu.closeAssociation(DCMSCU_RELEASE_ASSOCIATION);
        return result;
    }
}

OFCondition SCP::queryFilewithDataset(OFList<OFString>& files, DcmDataset dataset)
{
    OFList<OFString> allFiles;
    //获取查询目录下的所有文件
    getFiles(QueryDirectory, allFiles);
    DataItem queryItem(dataset);
    DcmList* queryList = queryItem.getElementList();
    if (queryList->empty() || allFiles.empty())
    {
        return OFCondition(EC_Normal);
    }

    for (auto file : allFiles)
    {
        DcmFileFormat fileformat;
        OFString val;//查询条件
        OFString value;
        OFCondition result = fileformat.loadFile(OFFilename(file));
        // 待查询的dataset
        DcmDataset* dataset = fileformat.getDataset();
        DcmObject* object;
        DcmTag tag;
        bool isequal = true;
        //遍历每个element
        queryList->seek(ELP_first);
        do
        {
            object = queryList->get();
            tag = object->getTag();//获取当前tag
            DcmElement* element;
            queryItem.findAndGetElement(tag, element);//获取tag对应的element
            element->getOFString(val, 0);//获取tag对应的value
            dataset->findAndGetOFString(tag, value);//获取当前查询文件的相同tag对应的value
            if (val != value)
            {
                isequal = false;
                break;
            }

        } while (queryList->seek(ELP_next));

        if (isequal)
        {
            files.push_back(file);
        }
    }
    return OFCondition(EC_Normal);
}

OFCondition SCP::ConnectToDest()
{
    initSCU();
    OFCondition result;
    /*初始化连接*/
    result = scu.initNetwork();
    if (result.bad())
    {
        DCMNET_ERROR("Unable to set up the network: " << result.text());
        return result;
    }
    result = scu.negotiateAssociation();
    if (result.bad())
    {
        DCMNET_ERROR("Unable to negotiate association: " << result.text());
        return result;
    }
    /*发送C-ECHO测试连接*/
    result = scu.sendECHORequest(0);
    if (result.bad())
    {
        DCMNET_ERROR("Could not process C-ECHO with the server:" << result.text());
        return result;
    }
    else
    {
        DCMNET_INFO("连接成功。\n");
    }
    return result;
}

OFBool SCP::stopAfterCurrentAssociation()
{
    if (isTmp)
        return OFTrue;
    else
        return OFFalse;
}

void getFiles(OFString path, OFList<OFString>& files)
{
    intptr_t hFile = 0;
    //文件信息
    struct _finddata_t fileinfo;
    OFString p;
    if ((hFile = _findfirst(p.assign(path).append("/*").c_str(), &fileinfo)) != -1)
    {
        do
        {
            //如果是目录,递归查找
            //如果不是,把文件绝对路径存入vector中
            if ((fileinfo.attrib & _A_SUBDIR))
            {
                if (strcmp(fileinfo.name, ".") != 0 && strcmp(fileinfo.name, "..") != 0)
                    getFiles(p.assign(path).append("/").append(fileinfo.name), files);
            }
            else
            {
                files.push_back(p.assign(path).append("/").append(fileinfo.name));
            }
        } while (_findnext(hFile, &fileinfo) == 0);
        _findclose(hFile);
    }
}

void SCP::setOutputDirectory(OFString path)
{
    OutputDirectory = path;
}

void SCP::setIsTmp(bool stat)
{
    isTmp = stat;
}

  调用SCP类的listen函数开启端口监听(这会阻塞线程)。在另一个线程中创建DcmSCU对象,向该SCP发送命令(仅支持C-ECHO、C-MOVE、C-STORE)。发送C-MOVE时需要另外启动一个线程并创建另一个SCP对象用于接收数据。IP地址和端口号请根据实际情况设置。

 

 

一、建立连接

DICOM网络连接建立在TCP基础上,使用IP地址和端口号通信。

1. SCP开始监听端口

2. 初始化TCP连接

3. SCU向SCP发送连接请求

4. SCP接收连接请求消息,查找是否有支持的服务

5. 若有支持的服务,SCP向SCU发送连接确认消息,SCU收到确认消息后DICOM连接建立。

6. 否则SCP向SCU发送连接拒绝消息,断开TCP连接

 

二、消息类型

DIMSE有C-Style风格和N-Style风格两种,PACS系统之间传输文件一般使用C-Style消息。

1. C-ECHO 用于确认连接是否建立

2. C-STORE 用于发送文件并存储

3. C-MOVE 用于查询和移动文件

4. C-GET 用于查询和拉取文件

5. C-FIND 用于查询文件

每种消息都有请求和确认两种。部分服务流程如下:

·C-STORE

SCU向SCP发送请求消息,消息中带有待存储的dicom数据文件,SCP收到消息后将数据文件存储在服务器,然后向SCU返回确认消息,包含处理结果。

·C-MOVE

SCU向SCP发送请求消息,消息中带有查询数据信息和移动目标的AETitle,SCP收到消息后,从服务器文件中查询是否有符合条件的文件,如果有,另外创建一个SCU,通过该SCU向目标发送C-STORE请求,等待C-STORE回应。一次C-MOVE操作中可能会包含多次C-STORE子操作。待所有符合条件的dicom文件都发送完毕后,关闭其创建的SCU,释放连接,然后向最初发送C-MOVE请求的SCU返回C-MOVE确认,包含C-MOVE的处理结果。

标签:DICOM,MOVE,文件传输,发送,DCMTK,SCU,OFString,SCP,STORE
From: https://www.cnblogs.com/cc-qt-dcmtk/p/17735580.html

相关文章

  • nc命令,扫描端口,文件传输
    nc是netcat的简写,是一个强大的网络工具作用:实现任意tcp/udp端口的侦听,nc可以作为server以tcp或udp方式侦听指令端口端口扫描,nc可以作为client发起tcp或udp连接机器间传输文件机器间网络测速 使用示例:1.验证某ip 的80端口通不通nc -zv 192.168.66.680 返回......
  • shell批量执行命令与文件传输脚本
    shell批量执行命令与文件传输脚本需求:对未进行主机信任操作的服务器进行批量操作实现:由于ssh只能在交互模式中输入服务器密码进行登录登操作,不便于进行大批量服务器进行巡检或日志采集。sshpass恰好又解决了这个问题,使用ssh-ppasswd可以实现命令行输入密码操作,便于进行规模......
  • JAVA应用XFire框架来实现WebServie的大文件传输功能之二(上传)
    xml文件:<?xmlversion="1.0"encoding="UTF-8"?><beansxmlns="http://xfire.codehaus.org/config/1.0"><service><name>HelloWorldService</name><namespace>http://localhost:8090......
  • JAVA应用XFire框架来实现WebServie的大文件传输功能之一(下载)
    下面是文件下载功能,可以实现大文件的断点续传,其原理是把服务端先把文件内容Byte[]转换成Base64编码字符串返回给客户端,然后客户端接收到后再把该Base64编码过的字符串转换成Byte[],最后写入文件。     至于断点续传的设计很简单,服务端留有一个定位参数,每次读取文件之前,都先定......
  • 十一、Nginx大文件传输配置
    某些业务场景中需要传输一些大文件,但大文件传输时往往都会会出现一些Bug,比如文件超出限制、文件传输过程中请求超时等,那么此时就可以在Nginx稍微做一些配置,先来了解一些关于大文件传输时可能会用的配置项:在传输大文件时,client_max_body_size、client_header_timeout、proxy_read_ti......
  • java文件传输简单方法
    java文件传输简单方法假设现在已经打包了一个文件(1233444333),要将这个文件传输给另一方:importjava.io.*;publicclassF_PasswordUnPassword{publicstaticvoidmain(String[]args)throwsException{ByteArrayOutputStreamarrOut=newByteArrayOutputStream();DataOutputStre......
  • 安全文件传输如何进行有效管控,从而促进业务的有序发展?
    随着信息化技术的不断发展,安全文件传输对于企业来说变得越来越重要,企业数据安全在近几年频繁发生,有不少企业都因数据泄漏而造成不同程度的损失,很多企业花费人力和财力采取各种措施,来确保自身数据安全和文件安全。然而,却往往忽略了,企业在平常的办公中,少不了外发文件的需要,尤其是现......
  • 医院信息化、数字医学影像、DICOM、PACS
    PACS系统适合卫生院、民营医院、二甲或以下公立医院的放射科、超声科使用。功能强大且简洁,性能优异,具备MPR(三维重建)、VR(容积重建)、胶片打印功能,能够快速部署。支持DR、CT、磁共振提供DICOM服务,支持临床医生工作站提供报告和影像浏览服务,系统支持与HIS、体检融合,从对方服务器获取......
  • Java前端上传文件后,将文件传输到后端,并将文件上传到FTP服务器上
    当我们在前端上传文件后,需要将文件传输到后端,并将文件上传到FTP服务器上。在Java中,我们可以使用ApacheCommonsNet库来实现FTP文件上传。下面是一个简单的示例,演示了如何在Java中实现前端上传文件后端接收并上传到FTP服务器的过程。前端上传文件首先,在前端页面中,我们需要一个文件......
  • 超实用的批量管理工具 pssh 和 window 文件传输工具 pscp
    目录一、概述1)pssh2)pscp二、pssh工具安装三、pssh命令的基本语法四、pscp工具安装1)Windows上安装2)Linux系统上安装五、pscp命令的基本语法1)从windows向linux传文件2)从linux传文件到windows一、概述pssh和pscp都是用于在计算机网络中进行批量操作的工具,但它们分......