DICOM 文件传输 基于DCMTK

发布时间 2023-09-28 13:57:44作者: 笼蒸土豆

DcmSCPdicom service class provider,相当于服务器

DcmSCUdicom service class user,相当于客户端

DIMSEdicom message service elementdicom连接中传递的消息单元

 

一、建立连接

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

1. SCP开始监听端口

2. 初始化TCP连接

3. SCUSCP发送连接请求

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

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

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

 

二、消息类型

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

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

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

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

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

5. C-FIND 用于查询文件

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

·C-STORE

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

·C-MOVE

  SCUSCP发送请求消息,消息中带有查询数据信息和移动目标的AETitleSCP收到消息后,从服务器文件中查询是否有符合条件的文件,如果有,另外创建一个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. SCUSCP发送连接请求

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

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

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

 

二、消息类型

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

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

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

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

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

5. C-FIND 用于查询文件

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

·C-STORE

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

·C-MOVE

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