DICOM 文件传输 基于DCMTK

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地址和端口号请根据实际情况设置。

posted @   笼蒸土豆  阅读(2038)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示