zoukankan      html  css  js  c++  java
  • DICOM医学图像处理:深入剖析Orthanc的SQLite,了解WADO & RESTful API

    背景:

            上一篇博文简单翻译了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),其注释中如此描述:

     

    [cpp] view plain copy
     
     print?
    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. *  ote 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:OrthancOrthancStoragefindex(其真实后缀为db3)。用SQLite可视化工具打开index文件,可以看到如下几张表:

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

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

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

    [cpp] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    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: est2.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中定义所示:

    [cpp] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    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文件给出定义,如下:

    [cpp] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    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标准,数据库的逻辑设计是很精妙的,后续可深入研究一下。

  • 相关阅读:
    JVM Ecosystem Report 2020
    TiDB 简介
    Docker镜像分层打包方案
    Promethues + Grafana + AlertManager使用总结
    Spring Boot自动注入原理
    Spring Boot 2.x 自定义Endpoint
    Oracle 等待事件 Enq: CF
    1000行MySQL学习笔记
    PostgreSQL DBA常用SQL查询语句
    MongoDB DBA常用的NoSQL语句
  • 原文地址:https://www.cnblogs.com/h2zZhou/p/6291664.html
Copyright © 2011-2022 走看看