上篇文章搭建好dicom服务器后,搭建Orthanc服务器-DICOM服务器_一只弱鶸的博客-CSDN博客
是时候学习一下与服务器的通讯了,Orthanc提供了一目了然的webui操作界面,查询、上传、查看、下载等操作,只需要点点鼠标就能实现,如下图所示
〇、可视化界面展示
1-主界面
2-病人列表界面
3-Series列表界面
4-viewer界面
相信这种界面操作对于没有经验的人(比如我),也能很快上手,但这显然不是学习的重点,因为项目中需要完成定制化的界面和操作,接下来就一起学习下使用DCMTK如何完成与DICOM服务器的通讯。
一、环境配置
DCMTK读取压缩格式的DICOM文件并使用Vtk显示_一只弱鶸的博客-CSDN博客
之前的博客讲到过DCMTK如何读取本地压缩格式的DICOM文件,这里相同的内容就不做赘述了。
二、基础理论知识
1-首先了解PACS服务架构
PACS(picture archiving and communication system)意为影像归档和通信系统。它是应用在医院影像科室的系统,主要的任务就是把日常产生的各种医学影像(包括核磁,CT,超声,各种X光机,各种红外仪、显微仪等设备产生的图像)通过各种接口(模拟,DICOM,网络)以数字化的方式海量保存起来,当需要的时候在一定的授权下能够很快的调回使用,同时增加一些辅助诊断管理功能。它在各种影像设备间传输数据和组织存储数据具有重要作用。 -摘自百度百科
理解PACS的服务架构和作用
医院PACS系统_家玲有点甜哦~~的博客-CSDN博客_pacs系统
图片摘自上述博客,侵删。
2- DICOM传输协议理解
1.1 DICOM协议简介及应用_DICOM医学影像的博客-CSDN博客_dicom
相信有HTTP协议基础的读者理解起来会轻松一点。
三、编码
和之前一样编码环境为WIndows 10、VS 2017、Qt、DCMTK 3.6.7(VS2017 64位下编译)。
参考getscu的实现代码,完成了最简单从DICOM服务器上下载DICOM文件的Demo,仅供参考,如有错误,请指出,谢谢!
#include <dcmtk/dcmnet/scu.h>
#include <dcmtk/dcmdata/dcpath.h>
typedef unsigned long OFCmdUnsignedInt;
typedef enum {
QMPatientRoot = 0,
QMStudyRoot = 1,
QMPatientStudyOnly = 2
} QueryModel;
static const char* querySyntax[3] = {
UID_GETPatientRootQueryRetrieveInformationModel,
UID_GETStudyRootQueryRetrieveInformationModel,
UID_RETIRED_GETPatientStudyOnlyQueryRetrieveInformationModel
};
static OFList<OFString> overrideKeys;
void applyOverrideKeys(DcmDataset *dataset)
{
/* replace specific keys by those in overrideKeys */
OFListConstIterator(OFString) path = overrideKeys.begin();
OFListConstIterator(OFString) endOfList = overrideKeys.end();
DcmPathProcessor proc;
proc.setItemWildcardSupport(OFFalse);
proc.checkPrivateReservations(OFFalse);
OFCondition cond;
while (path != endOfList)
{
cond = proc.applyPathWithValue(dataset, *path);
if (cond.bad())
{
}
path++;
}
}
void prepareTS(E_TransferSyntax ts,
OFList<OFString>& syntaxes)
{
/*
** We prefer to use Explicitly encoded transfer syntaxes.
** If we are running on a Little Endian machine we prefer
** LittleEndianExplicitTransferSyntax to BigEndianTransferSyntax.
** Some SCP implementations will just select the first transfer
** syntax they support (this is not part of the standard) so
** organize the proposed transfer syntaxes to take advantage
** of such behavior.
**
** The presentation contexts proposed here are only used for
** C-FIND and C-MOVE, so there is no need to support compressed
** transmission.
*/
switch (ts)
{
case EXS_LittleEndianImplicit:
/* we only support Little Endian Implicit */
syntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
break;
case EXS_LittleEndianExplicit:
/* we prefer Little Endian Explicit */
syntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
syntaxes.push_back(UID_BigEndianExplicitTransferSyntax);
syntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
break;
case EXS_BigEndianExplicit:
/* we prefer Big Endian Explicit */
syntaxes.push_back(UID_BigEndianExplicitTransferSyntax);
syntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
syntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
break;
#ifdef WITH_ZLIB
case EXS_DeflatedLittleEndianExplicit:
/* we prefer Deflated Little Endian Explicit */
syntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax);
syntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
syntaxes.push_back(UID_BigEndianExplicitTransferSyntax);
syntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
break;
#endif
default:
DcmXfer xfer(ts);
if (xfer.isEncapsulated())
{
syntaxes.push_back(xfer.getXferID());
}
/* We prefer explicit transfer syntaxes.
* If we are running on a Little Endian machine we prefer
* LittleEndianExplicitTransferSyntax to BigEndianTransferSyntax.
*/
if (gLocalByteOrder == EBO_LittleEndian) /* defined in dcxfer.h */
{
syntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
syntaxes.push_back(UID_BigEndianExplicitTransferSyntax);
} else
{
syntaxes.push_back(UID_BigEndianExplicitTransferSyntax);
syntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
}
syntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
break;
}
}
void download()
{
E_TransferSyntax opt_get_networkTransferSyntax = EXS_Unknown;
OFCmdUnsignedInt opt_maxPDU = ASC_DEFAULTMAXPDU;
int opt_dimse_timeout = 0;
int opt_acse_timeout = 30;
QueryModel opt_queryModel = QMStudyRoot;
T_DIMSE_BlockingMode opt_blockMode = DIMSE_BLOCKING;
const char *opt_peer = "172.16.31.18";
OFCmdUnsignedInt opt_port = 4242;;
const char *opt_peerTitle = "ORTHANC";
const char *opt_ourTitle = "ORTHANC";
OFList<OFString> fileNameList;
OFBool opt_showPresentationContexts = OFFalse;
E_TransferSyntax opt_store_networkTransferSyntax = EXS_Unknown;
DcmStorageMode opt_storageMode = DCMSCU_STORAGE_DISK;
OFString opt_outputDirectory = "e:/1/";
OFCmdUnsignedInt opt_repeatCount = 1;
overrideKeys.push_back("QueryRetrieveLevel=STUDY");
overrideKeys.push_back("StudyInstanceUID=1.2.840.113619.186.18420258241203197.20210618135644203.957");
OFList<OFString> syntaxes;
prepareTS(opt_get_networkTransferSyntax, syntaxes);
DcmSCU scu;
scu.setMaxReceivePDULength(opt_maxPDU);
scu.setACSETimeout(opt_acse_timeout);
scu.setDIMSEBlockingMode(opt_blockMode);
scu.setDIMSETimeout(opt_dimse_timeout);
scu.setAETitle(opt_ourTitle);
scu.setPeerHostName(opt_peer);
scu.setPeerPort(OFstatic_cast(Uint16, opt_port));
scu.setPeerAETitle(opt_peerTitle);
scu.setVerbosePCMode(opt_showPresentationContexts);
/* add presentation contexts for get and find (we do not actually need find...)
* (only uncompressed)
*/
scu.addPresentationContext(querySyntax[opt_queryModel], syntaxes);
/* add storage presentation contexts (long list of storage SOP classes, uncompressed) */
syntaxes.clear();
prepareTS(opt_store_networkTransferSyntax, syntaxes);
for (Uint16 j = 0; j < numberOfDcmLongSCUStorageSOPClassUIDs; j++)
{
scu.addPresentationContext(dcmLongSCUStorageSOPClassUIDs[j], syntaxes, ASC_SC_ROLE_SCP);
}
/* set the storage mode */
scu.setStorageMode(opt_storageMode);
if (opt_storageMode != DCMSCU_STORAGE_IGNORE)
{
scu.setStorageDir(opt_outputDirectory);
}
/* initialize network and negotiate association */
OFCondition cond = scu.initNetwork();
if (cond.bad())
{
exit(1);
}
cond = scu.negotiateAssociation();
if (cond.bad())
{
exit(1);
}
cond = EC_Normal;
T_ASC_PresentationContextID pcid = scu.findPresentationContextID(querySyntax[opt_queryModel], "");
if (pcid == 0)
{
exit(1);
}
/* do the real work, i.e. send C-GET requests and receive objects */
for (Uint16 repeat = 0; repeat < opt_repeatCount; repeat++)
{
size_t numRuns = 1;
DcmFileFormat dcmff;
DcmDataset *dset = dcmff.getDataset();
OFListConstIterator(OFString) it;
/* load first file, if there is one */
if (!fileNameList.empty())
{
numRuns = fileNameList.size();
it = fileNameList.begin();
cond = dcmff.loadFile((*it).c_str());
if (cond.bad())
{
exit(1);
}
dset = dcmff.getDataset();
}
OFList<RetrieveResponse*> responses;
/* for all files (or at least one run from override keys) */
for (Uint16 i = 0; i < numRuns; i++)
{
applyOverrideKeys(dset);
cond = scu.sendCGETRequest(pcid, dset, &responses);
if (cond.bad())
{
exit(1);
}
/* load next file if there is one */
if (numRuns > 1)
{
it++;
cond = dcmff.loadFile((*it).c_str());
if (cond.bad())
{
exit(1);
}
dset = dcmff.getDataset();
}
}
if (!responses.empty())
{
/* output final status report */
(*(--responses.end()))->print();
/* delete responses */
OFListIterator(RetrieveResponse*) iter = responses.begin();
OFListConstIterator(RetrieveResponse*) last = responses.end();
while (iter != last)
{
delete (*iter);
iter = responses.erase(iter);
}
}
}
scu.abortAssociation();
}
int main(int argc, char *argv[])
{
//echoscu 测试
// T_ASC_Network *net;
// ASC_initializeNetwork(NET_REQUESTOR, 0, 1000, &net);
// T_ASC_Parameters *params;
// ASC_createAssociationParameters(¶ms, ASC_DEFAULTMAXPDU);
// ASC_setAPTitles(params, "ECHOSCU", "ANY-SCP", NULL);
// ASC_setPresentationAddresses(params, "127.0.0.1", "172.16.31.18:4242");
// const char* ts[] = { UID_LittleEndianImplicitTransferSyntax};
// ASC_addPresentationContext(params, 1, UID_VerificationSOPClass, ts, 1);
// T_ASC_Association * assoc;
// if(ASC_requestAssociation(net, params, &assoc).good())
// {
// if(ASC_countAcceptedPresentationContexts(params) == 1)
// {
// DIC_US id = assoc->nextMsgID++;
// DIC_US status;
// DcmDataset *ds = NULL;
// DIMSE_echoUser(assoc, id, DIMSE_BLOCKING, 0, &status, &ds);
// delete ds;
// }
// }
// ASC_releaseAssociation(assoc);
// ASC_destroyAssociation(&assoc);
// ASC_dropNetwork(&net);
download();
return 0;
}
下载的文件不是dcm格式的,需要手动修改后缀名,才能正常查看。
这里还需要,搞懂几个名词:
AE Title:AE Title(Application Entity Title)是配置影像检查设备DICOM服务(Worklist、Storage、Print等)必不可少的参数之一。
Application Entity Title - The representation used to identify the DICOM nodes communicating between each other.
对于某一台影像检查设备,其各个DICOM服务可以对应不同的AE Title,当然这些DICOM服务也可以对应同一个AE Title。AE Title是一个字符串,但是这个字符串在我们要配置的RIS/PACS系统的网络中必须是唯一的。因此,AE Title是这个网络中某一个(或几个)DICOM服务的唯一标识。(摘自百度百科)
PeerHostname:服务器的主机名或IP地址
PeerPort:服务器的端口号这里为 4242 默认端口号为 104