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++ソースファイルのみ)
C-MOVE
参考資料1では実際にC-ECHO,C-FIND,C-MOVEの実装が示されているが,C-MOVE動作は実際には直接動作しない.のC-STOREの実装はこの3つの例を参考にすることができ,CT画像の記憶のみを考慮するため,実装も比較的簡単である.C-MOVEの実現過程を重点的に記録します.その前にC-MOVEの問題がどこでDCMTK開発ノート(四):C-MOVE操作の詳細を振り返ってみましょう.公式に与えられたコードは以下の通りです.
この例の問題は、C-MOVE操作の宛先が指定されていないことである.C-MOVE操作は、PACSに他のホストにデータを送信させることもできるからである.我々の応用では実際には,C−GETの機能,すなわちC−MOVEを送信するリクエスト者であり,データ受信者でもある.そのため、クライアントでSCPを開いてPACSが送信したデータを受信する必要があります.SCPは次のように設定されています.
SCPを開くにはscp.listen()メソッドを呼び出す必要がありますが、実際にはこの文がどこに並べられてもコードが正しく動作しないことがわかります.ループ待ちのため、私たちが送信したC-MOVEリクエストは1つ以上ありません.SCPはリスニングを維持する必要があります.scp.listen()が前にある場合、プログラムは待機に入ってリクエストを送信できません.その後、要求の送信時にPACSがSCPに接続できない場合は、エラーが発生します.だから正解はマルチスレッド方式を採用して、参考資料5をまねて、プロセス関数を書くことができます:
次に、C-MOVEリクエストを送信したwhileループの前後で、プロセスをそれぞれ開始および停止します.
問題は解けなければならない.
C-GET
次に、PACSが仮想マシンに配備されていない場合のC-GET操作を試みます.まず、コマンドラインツールを使用してC-GETのプロセスをシミュレートします.
通信ログは次のとおりです.
同じ接続では,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.
前の4つのブログでは、Visual Studio 2010環境でDcmtkライブラリを使用するために必要な設定と、Windows 10およびUbuntu環境でのdcmqrscpシミュレーションPACSの構成方法について説明しています.本文は前の基礎の上でC++を記録してDIMSE-Cの規定の5種類の操作を実現します:
参考資料
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.