背景:

上一篇博文简单翻译了Orthanc官网给出的CodeProject上“利用Orthanc Plugin SDK开发WADO插件”的博文,其中提到了Orthanc从0.8.0版本之后支持快速查询,而原本的WADO请求需要是直接借助于Orthanc内部的REST API逐级定位。那么为什么之前的Orthanc必须要逐级来定位WADO请求的Instance呢?新版本中又是如何进行改进的呢?此篇博文通过分析Orthanc内嵌的SQLite数据库,来剖析Orthanc的RESTful API机制,以及WADO服务的实现。

Orthanc UUID与DICOM UID:

1)Orthanc Plugin SDK模拟实现WADO Server

上一篇博文中提到的LocateStudy、LocateSeries、LocateInstanc函数都不是直接查询WADO请求传入的各级UID(StudyUID、SeriesUID、InstanceUID),而是通过内部构建出等同的RESTful API来实现。举个例子,测试DCM文件名为test1.dcm,其对应的三级UID分别是:

StudyUID=1.3.6.1.4.1.30071.6.176694098609799.4240639413125000,

SeriesUID=1.3.6.1.4.1.30071.6.176694098609799.4240639413125000.1,

InstanceUID(即SOP Instance UID)=2.16.840.114421.81623.9430067258.9493139258,正常的WADO协议规定的请求连接为:

http://localhost:8042/wado?requestType=WADO&studyUID=1.3.6.1.4.1.30071.6.176694098609799.4240639413125000&

seriesUID=1.3.6.1.4.1.30071.6.176694098609799.4240639413125000.1&

objectUID=2.16.840.114421.81623.9430067258.9493139258

按照常规方式来实现的话,应该是直接利用SQL语句在指定的数据库中直接搜索WADO Request中的三级UID,而在Orthanc Plugin SDK实现的WADO插件中,却是分级进行,详细流程如下:

Study级别:第一,LocateStudy函数中构建http://localhost:8042/studies请求,利用内置的REST API服务获得当前数据中所有的studies的UUID(后面会讲到该UUID与DICOM UID之间的转换关系);第二,LocateStudy中的每一个studyUUID,构造http://localhost:8042/studies/XXXX-XXXX-XXXX-XXXX,通过对比返回JSON数据中study["MainDicomTags"]["StudyInstanceUID"]标签值与WADO中的studyUID,实现定位Study的功能;

Series级别:与Study相同,先构造http://localhost:8042/series获取全部seriesUUID,然后针对每个seriesUUID构造http://localhost:8042/series/XXXX-XXXX-XXXX-XXXX,对比返回值中的series["MainDicomTags"]["SeriesInstanceUID"]与seriesUID,实现定位Series的功能;

Instance级别:先构造http://localhost:8042/instances获取全部instanceUUID,然后对每个instanceUUID构造http://localhost:8042/instances/XXXX-XXXX-XXXX-XXXX对比返回值中的instance["MainDicomTags"]["SOPInstanceUID"]与WADO请求中的objectUID,实现最终定位图像的目的。

2)Orthanc UUID与DICOM UID

上面的实现是不是很繁琐啊,哈哈。好在官方Plugin SDK说明博文中给出了最新版的定位方式,具体的实现可参见我上一篇博文(http://blog.csdn.net/zssureqh/article/details/41836885)。那么为何Orthanc起初需要如此繁琐的定位图像呢?这里我们先简单的分析一下Orthanc内部是如何来标记文件的唯一性的,后续章节再详细分析之前Orthanc模拟WADO服务为何如此繁琐。

在Orthanc源码中有这样一个类DicomInstanceHasher(定义在DicomInstanceHasher.h,实现在DicomInstanceHasher.cpp),其注释中如此描述:

  1. /**
  2. * This class implements the hashing mechanism that is used to
  3. * convert DICOM unique identifiers to Orthanc identifiers. Any
  4. * Orthanc identifier for a DICOM resource corresponds to the SHA-1
  5. * hash of the DICOM identifiers.
  6. * \note SHA-1 hash is used because it is less sensitive to
  7. * collision attacks than MD5. <a
  8. * href="http://en.wikipedia.org/wiki/SHA-256#Comparison_of_SHA_functions">[Reference]</a>
  9. **/

从描述中我们可以知道Orthanc内部时利用SHA1(百度百科:维基百科:)算法来计算出DCM文件的唯一标识的,具体计算过程为:

PatientID对应的UUID:即向SHA1计算函数中直接输入【PatientID】,获得SHA1值

StudyUID对应的UUID:向SHA1计算函数中输入【PatientID+”|"+StudyUID】,获得SHA1值

SeriesUID对应的UUID:向SHA1计算函数中输入【PatientID+”|"+StudyUID+”|"+SeriesUID】,获得SHA1值

InstanceUID对应的UUID:向SHA1计算函数中输入【PatientID+”|"+StudyUID+”|"+SeriesUID+”|"+InstanceUID】,获得SHA1值

这就是OrthancUUID与DICOM UID之间的转换关系,下一节讲解数据库时再给出真实的示例。

Orthanc SQLite介绍:

1)Orthanc SQLite数据库列表介绍:

Orthanc采用了SQLite嵌入式数据库,对数据库的操作在工程代码中集成,因此在使用过程中并未能感觉到数据库的管理,这也支撑了Orthanc主打的轻型、便捷、网络化优点。下面简单介绍一下Orthanc SQLite数据表的逻辑:

SQLite的数据库文件默认存储位置为:C:\Orthanc\OrthancStoragef\index(其真实后缀为db3)。用SQLite可视化工具打开index文件,可以看到如下几张表:

从表名称中可以推断出各表大致的用途:例如AttachedFiles是添加文件的记录、Changes可能为修改操作(删除、匿名化等)、DicomIdentifiers为DICOM文件标示符(各级UID)、ExportedResources可能为导出或上传操作、GlobalProperties应该是全局属性、MainDicomTags应该是Orthanc返回给REST API操作的JSON格式数据、Metadata是数据体、Resources应该是文件体标记(PatientRecyclingOrder暂时不清楚,请看下文分析)。

2)Orthanc主要数据操作类介绍:

Orthanc源码中有DatabaseWrapper类,其中有如下注释:

  1. /**
  2. * This class manages an instance of the Orthanc SQLite database. It
  3. * translates low-level requests into SQL statements. Mutual
  4. * exclusion MUST be implemented at a higher level.
  5. **/

说明该类是Orthanc操作SQLite数据库的封装类,具体的涉及到SQLite数据库底层的操作都由DatabaseWrapper来完成。与上节看到的index中的表对比,将DatabaseWrapper类主要函数分类:

数据表 DatabaseWrapper操作函数
AttachedFiles AddAttachment 
DeleteAttachment 
LookupAttachment 
ListAvailableAttachments
Resources CreateResource 
DeleteResource 
GetResourceType 
GetResourceCount 
LookupResource
Metadata DeleteMetadata 
GetAllMetadata 
GetMetadata 
GetMetadataAsInteger 
LookupMetadata 
SetMetadata

另外还会看到众多获取各表字段的函数,例如GetPublicId、GetChildrenPublicId等等。

Orthanc中SQLite实例测试:

在大致了解了Orthanc中SQLite数据库的基本结构后,进行一下实例测试。如博文(http://blog.csdn.net/zssureqh/article/details/41836885)所述,向Orthanc中添加数据有多种方式,命令行工具,REST API,以及网页。下面我们对Orthanc自带的Explorer和DCMTK工具包storescu.exe进行真实数据上传测试。

SQLite数据写入逻辑实例测试

1)Explorer中Drag & Drop测试:

先打开Orthanc的浏览界面:http://localhost:8042/app/explorer.html#upload

拖拽任意图像到浏览器内,单击【Start the upload】,直到出现绿色'【Done】,表明上传成功。

数据库变化如下:

2)storescu.exe测试:

上述利用Orthanc内嵌的Explorer成功上传并写入数据库。此次使用storescu.exe,把Orthanc当做Dicom Server查看数据写入情况,写入指令如下:

storescu.exe -d localhost 4242 -aet ZSSURE -aec ORTHANC c:\test2.dcm

完成后数据库变化如下:

SQLite查询逻辑测试:

上面利用两种方式来完成了添加数据到Orthanc内嵌SQLite数据库(还有REST API第三种方式,参见之前博文:,由于原理与Explorer中类同就不单独介绍了),并且观察到了数据库的真实变化,但是具体的字段含义此刻可能还不是很清楚,让我们利用REST API来读取数据库并尝试分析下其中的含义。

1)Patients:

curl http://localhost:8042/patients

返回结果如上图所示,通过对比上一节中观察到的数据库变化发现:返回的两个Patient UUID分别记录在Resources表中PublicId列的第4与8行,其对应的internalId分别为44和48。因此我们可以推断出Resources中应该是我们上传文件的记录,下面来验证一下我们的猜想。

根据上一节分析指导此处的publicId应该是DICOM UID对应的UUID,即SHA1计算值。打开在线计算SHA1网站:http://www.seacha.com/tools/sha1.html。按照上一节分析输入test1.dcm的各级UID,计算结果如下所示:

从图中我们可以看出在Resources表中的前四条记录按照级别深度分别存储的是InstanceUUID、SeriesUUID、StudyUUID、PatientUUID,这些UUID是由DICOM 各级UID进行SHA1计算所得。有兴趣的话可以验证一下后四条记录,自然也是相同的含义。至此我们搞清楚了Resources表的意义,是用于存储DICOM图像的UUID

2)Studies:

curl http://localhost:8042/studies

返回结果为,

即上述分析的Resources表中的每组的第三条记录,也就是表中的43和47行。

3)Series:

curl http://localhost:8042/series

返回结果为,

Resources表中每组记录的第二条,表中的42和46行。

4)Instances:

curl http://localhost:8042/instances

返回结果为,

Resources表中每组记录的第一条,表中的41和45行。

5)查看每个Patient内容:

curl http://localhost:8042/patients/64d6f8a0-ea0ffdb2-a14d1488-4fa7879c-2d9758d8

对比前面数据库的分析,发现大多数字段都可以直接在数据库中看到对应的值,如下图所示:

6)查看具体Instance内容

因为查看Study和Series级别的内容与查看Patient级别类似,就不啰嗦了,直接看一下具体Instance(即DICOM文件)的查询结果,输入指令:

curl http://localhost:8042/instances/064123d1-803dde30-f81071dc-cb2aad3b-bd246b7b

上述结果在数据库中都可以直接找到,如下图所示:

至此我们看到了熟悉的【SOP Instance UID】,原来存储在DicomIdentifiers表中。

从上述的多次实例测试我们也大致猜出来Orthanc SQLite数据库中各表的作用,Resources表中是利用SHA1来计算出UUID唯一标识我们的DCM文件;DicomIdentifiers表记录的是对应DCM文件的各级DICOM UID,想必这也是WADO协议中需要定位文件的必要参数;MainDicomTags表存储的是对应DCM文件的主要几种Tag,包括Group号、Element号,以及值域数据。各个表之间的关联是通过Resources表中的internalId来完成的,internalId是大多数表的主键(PK)。

到这里本文就可以结束了,已经达到了剖析Orthanc SQLite的目的,但是还并未清晰的看出REST API与WADO的区别。为此,也为了更好的了解Orthanc的操作流程,再补充一节,通过单步调试来深入分析一下Orthanc的实现机制,达到深入剖析的境界。

Orthanc SQLite总结:

前一篇博文中对Orthanc官方给出的Plugin SDK开发文档进行了简短的翻译,文档中指出在0.8.0版本之前,Orthanc是利用内建的RESTful API来模拟是实现WADO服务的,并非是直接响应浏览器发送过来的WADO请求。前文中已经介绍了如何具体编译和安装官方WadoPlugin.dll,这里在剖析SQLite的基础上采用单步调试的方式查看一下早期Orthanc是如何利用RESTful API来模拟实现WADO服务的。

RESTful API模拟WADO

官网给出的利用内建RESTful API仿真WADO的代码在WadoPlugin.cpp中的Wado函数内,其中最主要的是LocateStudy、LocateSeries和LocateInstance三个定位函数。下图是LocateStudy级别的单步调试结果:

从上图可以看出在LocateStudy函数内部,首先是利用DatabaseWrapper.cpp中的GetAllPublicId函数从SQLite数据库的Resources表中提取出全部的publicId,如我们上面分析,每一个上传的文件都有唯一对应的UUID格式的publicId。

随后,在LocateStudy函数内部,对前面返回的所有publicId进行循环遍历,针对每一个/studies/{publicId}进行资源定位,用到的函数是LookupResource(同样在DatabaseWrapper.cpp中)。通过下图中可以看出该函数从Resources表中根据publicId查询出internalId和resourceType两个字段。查看LookupResource函数参数type的类型ResourceType定义可知:Resources表中第二列字段存储的是publicId对应的资源级别,该级别按照DICOM3.0标准划分为Patient(=1)、Study(=2)、Series(=3)、Instance(=4)四级,如Enumeration.h中定义所示:

  1. enum ResourceType
  2. {
  3. ResourceType_Patient = 1,
  4. ResourceType_Study = 2,
  5. ResourceType_Series = 3,
  6. ResourceType_Instance = 4
  7. };

下面直接贴出调试的截图:

从截图中可以看出Orthanc中响应WADO请求的大致数据库检索流程,首先是在Resources表中查询所有的publicId(因为初次查询无法利用WADO请求中的studyID/seriesID/objectID计算出任何有效UUID);然后构造/studies/{id}形式的uri,利用RESTful API机制查询组合出各个级别的publicId,其各级之间的关系由表Resources中的parentId字段标明,而唯一性由主键internalId来决定。这也就是上述多次发起RESTful API查询数据库的主要原因;待获得了各级publicId和internalId后,就是从DicomIdentifiers表、MainDicomTags表和Metadata表中提取DICOM文件关键信息操作;最后自然就是将查询到的结果图像返回到浏览器端(可以DICOM格式或JPEG缩略图形式返回)。

【注】:在表Metadata中记录的type由Enumerations.h文件给出定义,如下:

  1. enum MetadataType
  2. {
  3. MetadataType_Instance_IndexInSeries = 1,
  4. MetadataType_Instance_ReceptionDate = 2,
  5. MetadataType_Instance_RemoteAet = 3,
  6. MetadataType_Series_ExpectedNumberOfInstances = 4,
  7. MetadataType_ModifiedFrom = 5,
  8. MetadataType_AnonymizedFrom = 6,
  9. MetadataType_LastUpdate = 7,
  10. // Make sure that the value "65535" can be stored into this enumeration
  11. MetadataType_StartUser = 1024,
  12. MetadataType_EndUser = 65535
  13. };

可以发现其中有RemoteAet类型,因此猜测可能跟DICOM 协议有关,用于记录上传端的AE Title,通过输入指令验证如下:

指令:storescu.exe -d localhost 4242 -aet ZSSURE -aec ORTHANC c:\Slice_0010.dcm

测试结果:

直接实现WADO

在分析了原有的效率较低的WadoPlugin查询方式后,我们按照同样的方式单步调试,查看新的Orthanc PluginSDK的查询过程。具体截图如下:

上述系列截图可以看出新的Orthanc Plugin SDK通过三步可以轻松从SQLite数据库中读取指定Instance的publicId(即上文说的UUID);获得了InstanceUUID后构造/instances/{id}类型的RESTful API uri来直接获取Orthanc数据库中的文件信息。如是减少了循环查询数据库的次数,提升了效率。仔细分析下来可以发现之所以原本的PluginSDK需要查询多次数据库是因为Orthanc中将DICOM文件及相关信息按照不同级别将信息分类存储,因此提取时需要分别定位然后将查询结果组合。另外打开Orthanc的Storage目录可以发现对于每个DCM文件Orthanc采用了publicId的两级目录方式来存储:第一级目录是文件的MD5值中的第一部分的前2个字节;第二级是后两个字节。如下图所示:

至此可以清楚地了解了Orthanc底层SQLite数据库的结构及相关操作,为了兼容RESTful API和DICOM3.0标准,数据库的逻辑设计是很精妙的,后续可深入研究一下。

DICOM医学图像处理:深入剖析Orthanc的SQLite,了解WADO & RESTful API的更多相关文章

  1. DICOM医学图像处理:storescp.exe与storescu.exe源码剖析,学习C-STORE请求

    转载:http://blog.csdn.net/zssureqh/article/details/39213817 背景: 上一篇专栏博文中针对PACS终端(或设备终端,如CT设备)与RIS系统之间w ...

  2. DICOM医学图像处理:DIMSE消息发送与接收“大同小异”之DCMTK fo-dicom mDCM

    背景: 从DICOM网络传输一文开始,相继介绍了C-ECHO.C-FIND.C-STORE.C-MOVE等DIMSE-C服务的简单实现,博文中的代码给出的实例都是基于fo-dicom库来实现的,原因只 ...

  3. [转]DICOM医学图像处理:Deconstructed PACS之Orthanc

    转载:http://blog.csdn.net/zssureqh/article/details/41424027 背景: 此篇博文介绍一个开源的.基于WEB的DICOM Server软件.该开源软件 ...

  4. DICOM医学图像处理:Deconstructed PACS之Orthanc

    背景: 此篇博文介绍一个开源的.基于WEB的DICOM Server软件.该开源软件完全使用C++编写,不依赖于第三方数据库(内置了SQLite数据库)或其他框架,支持RESTful API设计模式. ...

  5. DICOM医学图像处理:Orthanc Plugin SDK实现WADO服务

    背景: Orthanc是博主发现的一个很完美的DICOM和HTTP服务端开源软件,前几篇分别介绍了Orthanc的基本使用.Orthanc从0.8.0版本之后给出了Plugin SDK,通过该SDK可 ...

  6. DICOM医学图像处理:开源库mDCM与DCMTK的比較分析(一),JPEG无损压缩DCM图像

    背景介绍: 近期项目需求,须要使用C#进行最新的UI和相关DICOM3.0医学图像模块的开发.在C++语言下,我使用的是应用最广泛的DCMTK开源库,在本专栏的起初阶段的大多数博文都是对DCMTK开源 ...

  7. DICOM医学图像处理:Deconstructed PACS之Orthanc,Modification & Anonymization

    背景: 上篇博文为引子,介绍了一款神奇的开源PACS系统——Orthanc.本篇开始解读官方Cookbook中的相关内容,对于简单的浏览.访问和上传请阅读前篇博文.在常规的PACS系统中还未出现对于D ...

  8. DICOM医学图像处理:WEB PACS初谈四,PHP DICOM Class

    背景: 预告了好久的几篇专栏博文一直没有整理好,主要原因是早前希望搭建的WML服务器计划遇到了问题.起初以为参照DCMTK的官方文档wwwapp.txt结合前两天搭建的WAMP服务器可以顺利的实现WM ...

  9. DICOM医学图像处理:WEB PACS初谈

    背景: 周末看到了一篇原公司同事的文章,讲的是关于新的互联网形势下的PACS系统.正好上一篇专栏文章也提到了有想搭建一个worklist服务器的冲动,所以就翻箱倒柜将原本学生时代做课题时搭建的简易We ...

随机推荐

  1. day04_01 知识回顾、算术运算符

    ","和"+"的区别 除法运算,整除//,别名"地板除" 取余数 2**10 2的10次方 指数运算 指数运算符优先级要比乘法要高,所以先算 ...

  2. [整理]tar压缩下来为什么格式是.tar.gz

    前段时间打包,直接用tar命令压缩,压缩好的文件取名rar.同事用winrar打开发现一直报错. 经过查询发现,tar -cvzf压缩下来的格式其实应该是.tar.gz 但是格式怎么会这么奇怪呢?是压 ...

  3. Collection类及常用API

    Collection类及常用API Collection—类集工具类,定义了若干用于类集和映射的算法,这些算法被定义为静态方法,具体查看api文档; a)  public static void so ...

  4. CodeForces839D[莫比乌斯反演] Codeforces Round #428 (Div. 2)

    /*CodeForces839D[莫比乌斯反演]*/ #include <bits/stdc++.h> typedef long long LL; const LL MOD = 10000 ...

  5. 网抓(XML Http Request、VBA)实现

    第一种,先看VBA Public Function GetInfo(strMoblie As String) As String '创建对象 Dim xmlHttp As Object Set xml ...

  6. java面试题之spring aop中jdk和cglib哪个动态代理的性能更好?

    在jdk6和jdk7的时候,jdk比cglib要慢: 在jdk8的时候,jdk性能得到提升比cglib要快很多: 结论出自:https://www.cnblogs.com/xuliugen/p/104 ...

  7. Java面试题之Java中==和equals()和hashCode()的区别

    “==”: ==是运算符,用来比较两个值.两个对象的内存地址是否相等: “equals()”: equals是Object类的方法,默认情况下比较两个对象是否是同一个对象,内部实现是通过“==”来实现 ...

  8. java面试题之java中用到的线程调度算法是什么

    抢占式.一个线程用完CPU之后,操作系统会根据线程优先级.线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行. 操作系统中可能会出现某条线程常常获取到VPU控制权的情况,为了让某些优 ...

  9. CentOS7开启docker远程访问

    在 CentOS 中没有 /etc/default/docker,另外在 CentOS7 中也没有找到 /etc/sysconfig/docker这个配置文件. 在 /usr/lib/systemd/ ...

  10. poj 3468 线段树成段更新

    A Simple Problem with Integers Time Limit: 5000MS   Memory Limit: 131072K Total Submissions: 54012   ...