DCMTK开发笔记(五):C++ Demo 实现 DIMSE-C 基本操作

前言

前面的四个博客介绍了 Visual Studio 2010环境下使用Dcmtk库需要的设置以及在Windows10和Ubuntu环境下dcmqrscp模拟PACS的配置方法。本文将在前面的基础上记录C++实现DIMSE-C规定的五种操作:

  • C-ECHO
  • C-STORE
  • C-FIND
  • C-GET
  • C-MOVE
    的方法。其中C-GET操作在使用NAT模式的Ubuntu虚拟机(PACS)和主机Win10相互通信时不能直接使用(具体原因请参见前一节),而且C-MOVE操作可以完美代替C-GET的功能,实际在本文中并未实现。

参考资料

DcmSCU example program
DcmSCP Class Reference
DICOM医学图像处理:storescp.exe与storescu.exe源码剖析,学习C-STORE请求
DICOM C-GET vs C-MOVE
C++ 多线程

源代码

先给出所有代码:(只有这一个C++源文件)

/* 
*  Author:  Qin Yimin 
*  Date: 2019-12-13
*  Purpose: Test Useage of the DcmSCU class 
*/ 

#include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */ 
#include "dcmtk/dcmnet/diutil.h"
#include "dcmtk/dcmnet/scu.h" 
#include "dcmtk/dcmnet/scp.h"
#include "dcmtk/dcmnet/dstorscp.h"
#include <Windows.h> // 用于创建storeSCP线程接收数据

#define OFFIS_CONSOLE_APPLICATION "testscu" 

static OFLogger echoscuLogger = OFLog::getLogger("dcmtk.apps." OFFIS_CONSOLE_APPLICATION); 
static char rcsid[] = "$dcmtk:  v"  OFFIS_DCMTK_VERSION " " OFFIS_DCMTK_RELEASEDATE " $"; 

/* 与PACS建立连接需要设置的参数 */
// our application entity title used for calling the peer machine	本机使用的AET
#define APPLICATIONTITLE     "ACME1" 

// host name of the peer machine	SCP的主机名
#define PEERHOSTNAME         "Ubuntu-Qin" 

// TCP/IP port to connect to the peer machine	SCP开放的端口号,设置时请参考PACS配置文件
#define PEERPORT 11112 

// application entity title of the peer machine		SCP使用的AET,设置时请参考PACS配置文件
#define PEERAPPLICATIONTITLE "ACME_STORE" 

// MOVE destination AE Title	C-MOVE操作的目的AET
#define MOVEAPPLICATIONTITLE "ACME3" 

// C-STORE 操作中想要发送的文件名
# define STOREFILENAME "E:\\DCMTK\\PACS\\SCU\\database\\LI_GANG.CT.ABDOMEN_HX_CH_ABD_C_(ADULT).0006.0001.2019.10.13.22.03.30.948121.1070307945.IMA"

// C-GET 操作中想要存放图像的目录
# define STORAGEDIR "E:\\DCMTK\\PACS\\SCU\\getDest"

// C-STORE 或 C-GET 中用来接收数据的端口号
#define STORESCPPORT 1235

// C-MOVE 操作中想要存放图像的目录
# define MOVEDESTDIR "E:\\DCMTK\\PACS\\SCU\\moveDest"
/* 与PACS建立连接需要设置的参数 */

// 返回给定scu对象的描述上下文(Presentation Context)ID
DWORD WINAPI storeScpThread(LPVOID lpParameter){
	DcmStorageSCP* pSCP = (DcmStorageSCP*)lpParameter;
	pSCP->listen(); // 开启storeSCP监听C-MOVE请求
	return 0L;
}

static Uint8 findUncompressedPC(const OFString& sopClass, 
	DcmSCU& scu) 
{ 
	Uint8 pc; 
	pc = scu.findPresentationContextID(sopClass, UID_LittleEndianExplicitTransferSyntax); 
	if (pc == 0) 
		pc = scu.findPresentationContextID(sopClass, UID_BigEndianExplicitTransferSyntax); 
	if (pc == 0) 
		pc = scu.findPresentationContextID(sopClass, UID_LittleEndianImplicitTransferSyntax); 
	return pc; 
} 

int main(int argc, char *argv[]) 
{ 
	/* 设置建立连接需要的参数 */
	/* Setup DICOM connection parameters */ 
	OFLog::configure(OFLogger::DEBUG_LOG_LEVEL); // 使用DEBUG模式的日志,输出连接建立的详细信息
	DcmSCU scu;	// 创建一个DcmSCU对象
	// set AE titles 
	scu.setAETitle(APPLICATIONTITLE); // 本机使用的AET,需要根据PACS配置文件设置
	scu.setPeerHostName(PEERHOSTNAME); // SCP的主机名,如果是本机设置为 localhost
	scu.setPeerPort(PEERPORT); // SCP的端口号
	scu.setPeerAETitle(PEERAPPLICATIONTITLE); // SCP的AET,需要根据PACS配置文件设置
	/* 设置建立连接需要的参数 */

	// Use presentation context for FIND/MOVE in study root, propose all uncompressed transfer syntaxes 
	// 提供供SCP选择的传输语义(Transfer syntax)列表
	OFList<OFString> ts; 
	ts.push_back(UID_LittleEndianExplicitTransferSyntax); 
	ts.push_back(UID_BigEndianExplicitTransferSyntax); 
	ts.push_back(UID_LittleEndianImplicitTransferSyntax);

	// 根据抽象语义(abstract syntax)和传输语义列表构建描述上下文
	scu.addPresentationContext(UID_VerificationSOPClass, ts); // C-ECHO操作,abstract syntax为"VerificationSOPClass"
	scu.addPresentationContext(UID_CTImageStorage,ts); // C-STORE操作,本程序的目的是存储CT图像
	scu.addPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel, ts); // C-FIND操作,abstract syntax为"使用基于研究(Study)的查询/检索模型"
	scu.addPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel, ts); // C-MOVE操作,abstract syntax为"使用基于研究(Study)的查询/检索模型"
	// scu.addPresentationContext(UID_GETStudyRootQueryRetrieveInformationModel, ts);	// C-GET操作,abstract syntax为"使用基于研究(Study)的查询/检索模型"

	/* 与PACS系统建立连接 */
	/* Initialize network */ 
	OFCondition result = scu.initNetwork(); 
	if (result.bad()) // 网络初始化失败
	{ 
		DCMNET_ERROR("Unable to set up the network: " << result.text()); 
		return 1; 
	} 
	/* Negotiate Association */ 
	result = scu.negotiateAssociation(); 
	if (result.bad()) // 无法建立连接
	{ 
		DCMNET_ERROR("Unable to negotiate association: " << result.text()); 
		return 1; 
	} 
	/* 与PACS系统建立连接 */

	/* Let's look whether the server is listening: 
	Assemble and send C-ECHO request 
	*/ 
	result = scu.sendECHORequest(0); 
	if (result.bad()) // C-ECHO操作失败,应检查SCP的hostname和port设置
	{ 
		DCMNET_ERROR("Could not process C-ECHO with the server: " << result.text()); 
		return 1; 
	} 


	/* C-STORE 操作 */
	/* Assemble and send C-STORE request */ 
	T_ASC_PresentationContextID presID = findUncompressedPC(UID_CTImageStorage, scu); 
	if (presID == 0) 
	{ 
		DCMNET_ERROR("There is no uncompressed presentation context for C-STORE"); 
		return 1; 
	} 
	Uint16 rspStatusCode; 
	result = scu.sendSTORERequest(presID,STOREFILENAME,NULL,rspStatusCode);
	if (result.bad()){
		DCMNET_ERROR("C-STORE Operation failed.");
	}
	else{
		DCMNET_INFO("C-STORE Operation completed successfully.");
	}

	/* C-Find 操作 */
	/* Assemble and send C-FIND request */ 
	OFList<QRResponse*> findResponses; 
	DcmDataset req; 
	req.putAndInsertOFStringArray(DCM_StudyID,"");
	req.putAndInsertOFStringArray(DCM_StudyDate,"");
	req.putAndInsertOFStringArray(DCM_QueryRetrieveLevel, "STUDY"); 
	req.putAndInsertOFStringArray(DCM_StudyInstanceUID, ""); 

	presID = findUncompressedPC(UID_FINDStudyRootQueryRetrieveInformationModel, scu);
	if (presID == 0) 
	{ 
		DCMNET_ERROR("There is no uncompressed presentation context for Study Root FIND"); 
		return 1; 
	} 
	result = scu.sendFINDRequest(presID, &req, &findResponses); 
	if (result.bad()) {
		DCMNET_ERROR("Unable to send find request successfully.");
		return 1; 
	}
	else {
		// 最后一个findResponse通常只标志find请求成功并且数据信息已全部发送
		DCMNET_INFO("There are " << findResponses.size()-1 << " studies available");
	}
	/* C-Find 操作 */

	/** C-MOVE 操作 */
	/* Assemble and send C-MOVE request, for each study identified above*/ 
	presID = findUncompressedPC(UID_MOVEStudyRootQueryRetrieveInformationModel, scu); 
	if (presID == 0) 
	{ 
		DCMNET_ERROR("There is no uncompressed presentation context for Study Root MOVE"); 
		return 1; 
	} 
	OFListIterator(QRResponse*) study = findResponses.begin(); 
	Uint32 studyCount = 1; 
	OFBool failed = OFFalse; 

	/* 设置SCP来接收数据 */
	DcmStorageSCP scp; // 新建一个DcmStorageSCP对象
	scp.setAETitle(MOVEAPPLICATIONTITLE); // 设置AET为C-MOVE操作指定的AET
	scp.setPort(STORESCPPORT); // 设置storeSCP监听的端口号,需要与PACS的配置文件中AET保持一致
	scp.setVerbosePCMode(OFFalse); // 设置Verbose模式便于调试
	scp.addPresentationContext(UID_CTImageStorage, ts); // 设置接收PC的abstract syntax和transfer syntax
	scp.setOutputDirectory(MOVEDESTDIR);

	HANDLE storeScpThreadHandle = CreateThread(NULL, 0, storeScpThread, &scp, 0, NULL);
    


	// Every while loop run will get all image for a specific study 
	while (study != findResponses.end() && result.good())
	{ 
		// be sure we are not in the last response which does not have a dataset 
		if ( (*study)->m_dataset != NULL) 
		{ 
			OFString studyInstanceUID; 
			result = (*study)->m_dataset->findAndGetOFStringArray(DCM_StudyInstanceUID, studyInstanceUID); 
			// only try to get study if we actually have study instance uid, otherwise skip it 
			if (result.good()) 
			{ 
				req.putAndInsertOFStringArray(DCM_StudyInstanceUID, studyInstanceUID); 
				// fetches all images of this particular study 

				result = scu.sendMOVERequest(presID, MOVEAPPLICATIONTITLE, &req, NULL /* we are not interested into responses*/); 

				if (result.good()) 
				{ 
					DCMNET_INFO("Received study #" << std::setw(7) << studyCount << ": " << studyInstanceUID); 
					studyCount++; 
				} 
				/*result = scp.receiveMOVERequest(NULL, 0, NULL, MOVEDESTDIR);
				if ( result.bad() ){
				DCMNET_ERROR("Receiving move request failed.");
				}*/
			}
		} 
		study++;
	} 
	//关闭线程
    CloseHandle(storeScpThreadHandle);

	if (result.bad()) 
	{ 
		DCMNET_ERROR("Unable to retrieve all studies: " << result.text()); 
	}
	while (!findResponses.empty())
	{
		delete findResponses.front();
		findResponses.pop_front();
	}
	/* C-MOVE 操作 */
	
	/* Release association */ 
	scu.closeAssociation(DCMSCU_RELEASE_ASSOCIATION);

	return 0; 
}

C-MOVE

参考资料1中实际上已经给出了C-ECHO、C-FIND和C-MOVE的实现,但是C-MOVE操作实际上并不能直接运行。。。C-STORE的实现可以参考这三个例子,由于我们只考虑CT图像的存储,实现起来也比较简单。所以着重记录一下C-MOVE的实现过程。在这之前先回顾一下C-MOVE的问题出在哪里 DCMTK开发笔记(四):C-MOVE操作详解
官方给出的代码如下:

/* Assemble and send C-MOVE request, for each study identified above*/ 
  presID = findUncompressedPC(UID_MOVEStudyRootQueryRetrieveInformationModel, scu); 
  if (presID == 0) 
  { 
    DCMNET_ERROR("There is no uncompressed presentation context for Study Root MOVE"); 
    return 1; 
  } 
  OFListIterator(QRResponse*) study = findResponses.begin(); 
  Uint32 studyCount = 1; 
  OFBool failed = OFFalse; 
  // Every while loop run will get all image for a specific study 
  while (study != findResponses.end() && result.good())
  { 
    // be sure we are not in the last response which does not have a dataset 
    if ( (*study)->m_dataset != NULL) 
    { 
      OFString studyInstanceUID; 
      result = (*study)->m_dataset->findAndGetOFStringArray(DCM_StudyInstanceUID, studyInstanceUID); 
      // only try to get study if we actually have study instance uid, otherwise skip it 
      if (result.good()) 
      { 
        req.putAndInsertOFStringArray(DCM_StudyInstanceUID, studyInstanceUID); 
        // fetches all images of this particular study 
        result = scu.sendMOVERequest(presID, MOVEAPPLICATIONTITLE, &req, NULL /* we are not interested into responses*/); 
        if (result.good()) 
        { 
          DCMNET_INFO("Received study #" << std::setw(7) << studyCount << ": " << studyInstanceUID); 
          studyCount++; 
        } 
      }
    } 
    study++;
  } 
  if (result.bad()) 
  { 
    DCMNET_ERROR("Unable to retrieve all studies: " << result.text()); 
  }
  while (!findResponses.empty())
  {
    delete findResponses.front();
    findResponses.pop_front();
  }

这个例子的问题在于,并没有指定C-MOVE操作的目的地,因为C-MOVE操作也可以让PACS发送数据给另一台主机。在我们的应用中实际是要实现C-GET的功能,即发送C-MOVE的请求者同时也是数据接收者。为此,我们需要在客户端开启一个SCP来接收PACS发送的数据。SCP设置如下:

	/* 设置SCP来接收数据 */
	DcmStorageSCP scp; // 新建一个DcmStorageSCP对象
	scp.setAETitle(MOVEAPPLICATIONTITLE); // 设置AET为C-MOVE操作指定的AET
	scp.setPort(STORESCPPORT); // 设置storeSCP监听的端口号,需要与PACS的配置文件中AET保持一致
	scp.setVerbosePCMode(OFFalse); // 设置Verbose模式便于调试
	scp.addPresentationContext(UID_CTImageStorage, ts); // 设置接收PC的abstract syntax和transfer syntax
	scp.setOutputDirectory(MOVEDESTDIR);

开启SCP需要调用scp.listen()方法,但是实践发现这一语句摆在哪里代码都不能正确运行。原因是循环等待造成的,我们发送的C-MOVE请求不止一个,SCP需要保持监听,如果scp.listen()在前,程序进入等待无法发送请求;如果在后,发送请求时PACS无法和SCP建立连接,又会出错。所以正解是采用多线程的方式,模仿参考资料5,可以写出进程函数:

DWORD WINAPI storeScpThread(LPVOID lpParameter){
	DcmStorageSCP* pSCP = (DcmStorageSCP*)lpParameter;
	pSCP->listen(); // 开启storeSCP监听C-MOVE请求
	return 0L;
}

然后在发送C-MOVE请求的while循环体前后分别启动和关闭进程:

HANDLE storeScpThreadHandle = CreateThread(NULL, 0, storeScpThread, &scp, 0, NULL);
/* while...sendMoveRequest*/
CloseHandle(storeScpThreadHandle);

问题得解。

C-GET

下面尝试在PACS未部署在虚拟机的情况下实现C-GET操作。首先使用命令行工具模拟C-GET的过程:

getscu localhost 11112 -v -S -aec ACME_STORE -aet ACME1 -k QueryRetrieveLevel=STUDY -k StudyDate -k StudyDescription -k StudyInstanceUID -od E:\DCMTK\PACS\SCU\getDest

通信日志如下:

I: Requesting Association
I: Association Accepted (Max Send PDV: 16372)
I: Sending C-GET Request (MsgID 1)
I: Received C-STORE Request (MsgID 1)
W: DICOM file already exists, overwriting: E:\DCMTK\PACS\SCU\getDest\CT.1.3.12.2.1107.5.1.4.73473.30000013061923223125000058319
I: Sending C-STORE Response (Success)
I: Received C-GET Response (Pending)
I: Received C-STORE Request (MsgID 2)
W: DICOM file already exists, overwriting: E:\DCMTK\PACS\SCU\getDest\CT.2.16.840.1.113662.2.1.4519.41582.4105152.419990505.410523251
I: Sending C-STORE Response (Success)
I: Received C-GET Response (Pending)
I: Received C-GET Response (Success)
I: Final status report from last C-GET message:
I:   Number of Remaining Suboperations : 0
I:   Number of Completed Suboperations : 2
I:   Number of Failed Suboperations    : 0
I:   Number of Warning Suboperations   : 0
I: Releasing Association

可以看到在同一个连接中,双方相互交换C-GET和C-STORE信息,这时客户端不仅仅是C-GET操作的SCU,同时也会C-STORE的SCP。情况比我们想象的要复杂。。。
how can I use dcmtk for C-Get?
DICOM:C-GET与C-MOVE对比剖析
PACS connection 这是一个没什么卵用但很好玩的帖子hh
开发人员给出了较详细的解释:

For C-GET to work, the SCU needs to negotiate one of the C-GET SOP classes with SCU role and, in the same association, negotiate some of the C-STORE (Storage) SOP classes with SCP role. This requires the optional SCP/SCU role negotiation sub-item to be used in the A-ASSOCIATE protocol, something which the SCU developer possibly “forgot” to implement. The complexity of the SCP/SCU role negotiation is also the main reason why almost nobody is using C-GET in practice.

所以,看起来C-GET好像不是特别有必要了hh。

  • 8
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
DCMTK是一个用于医学影像处理的开源软件,支持DICOM(Digital Imaging and Communications in Medicine)标准。实现C-MOVE是其中的一个功能,它允许从DICOM服务器上获取医学影像数据。 下面是一个使用DCMTK简单实现C-MOVE的代码示例: ```cpp #include "dcmtk/dcmnet/assoc.h" #include "dcmtk/dcmnet/cond.h" #include "dcmtk/dcmnet/dicom.h" int main() { // 初始化DCMTK库 DcmNetSCU.initializeNetwork(); // 创建一个新的DICOM关联 T_ASC_Parameters* params; ASC_createAssociationParameters(&params); ASC_setAPTitles(params, "C-MOVE_SCU", "C-MOVE_SCP", NULL); ASC_setTransportLayerType(params, TL_TCP); ASC_setAcceptor(params, OFTrue); // 连接DICOM服务器 const char* serverHostname = "127.0.0.1"; const int serverPort = 11112; T_ASC_Association* assoc; ASC_requestAssociation(params, &assoc, serverHostname, serverPort); if(ASC_associationAccepted(assoc)) { // 发送C-MOVE请求 DcmDataset* queryDataSet = new DcmDataset(); // 设置查询条件,例如StudyInstanceUID、SeriesInstanceUID等 queryDataSet->putAndInsertString(DCM_QueryRetrieveLevel, "STUDY"); queryDataSet->putAndInsertString(DCM_StudyInstanceUID, "1.2.3.4.5"); T_DIMSE_C_MoveRQ moveRequest; memset(&moveRequest, 0, sizeof(moveRequest)); moveRequest.MessageID = assoc->nextMsgID++; strncpy(moveRequest.AffectedSOPClassUID, UID_FINDStudyRootQueryRetrieveInformationModel, sizeof(moveRequest.AffectedSOPClassUID)); OFString moveDest = "C-MOVE_SCP"; // C-MOVE_SCP的AETitle OFString moveDestHost; OFString moveDestPort; OFStandard::strExtractSubstring(moveDest, moveDestHost, 0, OFStandard::strChr(moveDest, ':')); OFStandard::strExtractSubstring(moveDest, moveDestPort, OFStandard::strChr(moveDest, ':') + 1, OFString_npos); strncpy(moveRequest.MoveDestination, moveDestHost.c_str(), sizeof(moveRequest.MoveDestination)); moveRequest.MoveDestination[15] = '\0'; moveRequest.Priority = DIMSE_PRIORITY_LOW; moveRequest.Identifier = queryDataSet; DIMSE_sendMoveRequest(assoc, &moveRequest, NULL, NULL, NULL); // 处理C-MOVE响应 T_DIMSE_C_MoveRSP* moveResponse; while(DIMSE_receiveCommand(assoc, DIMSE_NONBLOCKING, 0) == DIMSE_NORMAL) { DIMSE_receiveDataSetInMemory(assoc, DIMSE_NONBLOCKING, NULL, NULL, NULL); DIMSE_getResponse(assoc, params, &moveResponse); // 解析响应,可以获取到影像数据 } delete moveResponse; delete queryDataSet; } else { // 关联未被接受 } // 关闭关联并清理资源 ASC_releaseAssociation(assoc); ASC_destroyAssociation(&assoc); // 清理DCMTK库 DcmNetSCU.cleanupNetwork(); return 0; } ``` 以上代码会连接到一个运行在本地的DICOM服务器,并发送一个C-MOVE请求,然后接收服务器返回的C-MOVE响应。在发送请求时,我们需要设置查询的条件,例如StudyInstanceUID、SeriesInstanceUID等来指定要获取的医学影像数据。在响应处理时,可以解析返回的数据集以获取影像数据。 以上是一个很简单的C-MOVE实现示例,实际应用中可能会有更多的细节和错误处理。使用DCMTK库可以方便地实现DICOM相关的功能,包括C-MOVE等。
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值