DCMTK開発ノート(五):C++Demo DIMSE-C基本操作を実現

27216 ワード

前言
前の4つのブログでは、Visual Studio 2010環境でDcmtkライブラリを使用するために必要な設定と、Windows 10およびUbuntu環境でのdcmqrscpシミュレーションPACSの構成方法について説明しています.本文は前の基礎の上でC++を記録してDIMSE-Cの規定の5種類の操作を実現します:
  • C-ECHO
  • C-STORE
  • C-FIND
  • C-GET
  • C-MOVEの方法.ここで、C-GETオペレーションは、NATモードのUbuntu仮想マシン(PACS)とホストWin 10との通信時に直接使用できない(具体的には前節を参照).また、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  //     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 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 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の実装はこの3つの例を参考にすることができ,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リクエストは1つ以上ありません.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.