從DICOM網絡傳輸一文開始,相繼介紹了C-ECHO、C-FIND、C-STORE、C-MOVE等DIMSE-C服務的簡單實現,博文中的代碼給出的實例都是基於fo-dicom庫來實現的,緣由只有一個:基於C#的fo-dicom庫具備高封裝性。對於初學者來講實現大多數的DIMSE-C、DIMSE-N服務幾乎都是「傻瓜式」操做——構造C-XXX-RQ、N-XXX-RQ而後綁定相應的OnResponseReceived處理函數便可。本博文但願在前幾篇預熱的基礎上,對比DCMTK、fo-dicom、mDCM三種庫構建DIMSE消息的具體操做,來分析一下三者對於DIMSE消息的發送和接收的實現,爲後續搭建簡易版的Dicom Server服務器作準備。php
DIMSE,是DICOM Message Service Element的簡稱。DICOM3.0第7部分指出:DIMSE爲對等DICOM應用實體進行醫學影像及相關信息交換提供了一種應用服務元素定義(Application Service Element),包括服務和協議(DIMSE Service 和DIMSE Protocol)。html
DIMSE基於DIMSE協議來提供服務,DIMSE協議規定了構造消息必需的編碼規則。一條DICOM MESSAGE由固定的指令集合(Command Set),外加可選擇的數據集合(Data Set)構成,以下截圖所示:mysql
能夠簡單的理解爲Command Set就是本博文即將要介紹的各類服務的請求和應答消息;而Data Set能夠認爲相似於DCM後綴的文件,是咱們但願在對等DICOM實體間進行傳輸的信息。可是從本質上來講Command Set和Data Set二者都遵循DICOM3.0協議中IOD的定義,都是「以(Group Number,Element Number)對」來進行標記的Data Element元素的集合,更形象一點的說明可參考早期的博文(http://blog.csdn.net/zssureqh/article/details/9275271)。linux
DIMSE Protocol指出Message可能會分片(fragmented,這與傳統的TCP/IP中的概念相似),消息的具體傳輸是基於ASSOCIATE(DICOM3.0第8部分)中的P-DATA Service(博文http://blog.csdn.net/zssureqh/article/details/41016091中有介紹)。git
DIMSE服務因操做SOP類型的不一樣分爲DIMSE-C Services和DIMSE-N Services,DIMSE-C服務支持在對等DICOM實體間進行Composite SOP Instance操做,主要包括C-ECHO、C-FIND、C-STORE、C-MOVE、C-GET等;而DIMSE-N服務支持Normalized SOP Instance操做,主要包括N-EVENT-REPORT、N-GET、N-SET、N-CREATE、N-ACTION、N-DELETE。程序員
從上圖能夠看出DIMSE-C服務只提供操做服務,即對等DICOM實體一方請求另外一方對Composite SOP Instance進行操做(operation);而DIMSE-N服務除了提供操做之外,還提供通知(notification)服務。DIMSE中的全部操做和通知都是確認服務(confirmed services),即一方發出的請求都須要獲得對方的應答(原文:All DIMSE operations and notifications are confirmed services. The performing DIMSE-service-user shall report the response of each operation or notificationover the same Association on which the operation or notification was invoked.)。每種服務具體方式不一樣,例如某些操做可能會觸發後續的子操做、某些操做可能須要多個響應等等,以下圖:github
DIMSE-C服務在醫學領域應用最普遍,常見的PACS、HIS、RIS、LIS等系統都會用到,而DIMSE-N服務主要應用在MPPS和DICOM打印中,平常學習中可能沒有實際應用和測試的機會,所以這裏就暫時不介紹,主要以DIMSE-C消息的構造爲主,來分別介紹三種庫的具體操做。算法
博文http://blog.csdn.net/zssureqh/article/details/41016091以前簡單介紹了一下DCMTK對於網絡傳輸方面的封裝,更多的是偏重於協議的各層(Layer),例如對最底層的基於TCP/IP的Dicom Upper Layer的封裝以DUL_爲前綴;對實體鏈接層的封裝以ASC_爲前綴;最頂層的是DIMSE層,以DIMSE_爲前綴。本博文會從DIMSE Services中的DIMSE-C各類消息入手,介紹DCMTK對於消息的封裝和操做:sql
DCMTK開源庫相較於其餘二者來講最大的優點是有完整的說明文檔、穩定的維護團隊,同時也有成功的商業產品。在源碼中也給出了各類服務工具包,前面的好多博文都已經介紹過DCMTK的工具包,例如針對於C-ECHO的echoscu.exe(博文後續的工程實例是用dcmqrscp.exe做爲mini DICOM服務端進行測試的)。數據庫
DCMTK對DIMSE-C中的各類消息的定義在dimse.h頭文件中,其中C-ECHO-RQ消息定義以下:
/* C-ECHO */ struct T_DIMSE_C_EchoRQ { DIC_US MessageID; /* M */ DIC_UI AffectedSOPClassUID; /* M */ T_DIMSE_DataSetType DataSetType; /* M */ } ; struct T_DIMSE_C_EchoRSP { DIC_US MessageIDBeingRespondedTo; /* M */ DIC_UI AffectedSOPClassUID; /* U(=) */ T_DIMSE_DataSetType DataSetType; /* M */ DIC_US DimseStatus; /* M */ unsigned int opts; /* which optional items are set */ #define O_ECHO_AFFECTEDSOPCLASSUID 0x0001 } ;
dimse.h中對於每一種DIMSE-C服務的請求消息(request)和響應消息(response)都給出了定義,並以union方式來統一了DICOM Message結構,以下所示:
/* * Composite DIMSE Message */ struct T_DIMSE_Message { T_DIMSE_Command CommandField; /* M */ union { /* requests */ T_DIMSE_C_StoreRQ CStoreRQ; T_DIMSE_C_EchoRQ CEchoRQ; T_DIMSE_C_FindRQ CFindRQ; T_DIMSE_C_GetRQ CGetRQ; T_DIMSE_C_MoveRQ CMoveRQ; T_DIMSE_C_CancelRQ CCancelRQ; T_DIMSE_N_EventReportRQ NEventReportRQ; T_DIMSE_N_GetRQ NGetRQ; T_DIMSE_N_SetRQ NSetRQ; T_DIMSE_N_ActionRQ NActionRQ; T_DIMSE_N_CreateRQ NCreateRQ; T_DIMSE_N_DeleteRQ NDeleteRQ; /* responses */ T_DIMSE_C_StoreRSP CStoreRSP; T_DIMSE_C_EchoRSP CEchoRSP; T_DIMSE_C_FindRSP CFindRSP; T_DIMSE_C_GetRSP CGetRSP; T_DIMSE_C_MoveRSP CMoveRSP; T_DIMSE_N_EventReportRSP NEventReportRSP; T_DIMSE_N_GetRSP NGetRSP; T_DIMSE_N_SetRSP NSetRSP; T_DIMSE_N_ActionRSP NActionRSP; T_DIMSE_N_CreateRSP NCreateRSP; T_DIMSE_N_DeleteRSP NDeleteRSP; } msg; };
DICOM3.0第7部分中有關於C-ECHO消息的參數說明以及具體指令編碼,正如前文所述Command一樣也是以(Group Number,Element Number)標記的Data Element元素的集合,所以按照DICOM3.0標準中的要求只要向C-ECHO-RQ或者C-ECHO-RSP指令中插入規定的Data Element元素便可。
如上圖所示,構造T_DIMSE_CEchoRQ須要填充MessageID/Affected SOP Class UID等,具體構造代碼以下:(代碼封裝在DIMSE_echoUser函數中)
T_DIMSE_Message req, rsp; T_ASC_PresentationContextID presID; const char *sopClass = UID_VerificationSOPClass; bzero((char*)&req, sizeof(req)); bzero((char*)&rsp, sizeof(rsp)); req.CommandField = DIMSE_C_ECHO_RQ; req.msg.CEchoRQ.MessageID = msgId; strcpy(req.msg.CEchoRQ.AffectedSOPClassUID, sopClass); req.msg.CEchoRQ.DataSetType = DIMSE_DATASET_NULL;
上面代碼中的rsp與咱們本身構建的req相似,惟一不一樣的是req是在C-ECHO SCU端構造,而rsp是在C-ECHO SCP端構造並經過網絡傳送過來的。
(具體的測試代碼可參見博文後文給出的鏈接)
下面咱們看一下比較複雜的消息C-FIND,相較於C-ECHO消息,C-FIND中須要給出咱們但願查詢的目標屬性列表(記住:一樣也是一個DcmDataset類型,即Dicom Element集合)。
C-FIND-RQ消息的構造代碼以下:
//定義臨時變量 T_ASC_PresentationContextID presId; T_DIMSE_C_FindRQ req; T_DIMSE_C_FindRSP rsp; DcmFileFormat dcmff; OFString temp_str; presId=ASC_findAcceptedPresentationContextID(assoc,abstractSyntax); //構造C-FIND-RQ消息 bzero(OFreinterpret_cast(char*, &req), sizeof(req)); strcpy(req.AffectedSOPClassUID,abstractSyntax); req.DataSetType=DIMSE_DATASET_PRESENT; req.Priority=DIMSE_PRIORITY_LOW; req.MessageID=assoc->nextMsgID++; //構造數據體,即咱們具體但願在C-FIND SCP端得到的信息 DcmDataset* dcmdataset=new DcmDataset(); dcmdataset->putAndInsertString(DCM_StudyInstanceUID,""); dcmdataset->putAndInsertString(DCM_StudyDate,""); dcmdataset->putAndInsertString(DCM_QueryRetrieveLevel,"STUDY"); DcmDataset *statusDetail = NULL; //在DIMSE_findUser內部會將dcmdataset數據合併到req中,統一構成T_DIMSE_Message OFCondition cond=DIMSE_findUser(assoc,presId,&req,dcmdataset,NULL,NULL,blockMode,dimse_timeout,&rsp,&statusDetail);
上述代碼比較複雜的是須要構造參數列表中的Identifier元素,該元素包含了咱們但願從C-FIND SCP服務端提供查詢得到的屬性,上面選擇了STUDY級別的查詢,所以須要添加DCM_QueryRetrieveLevel元素、StudyInstanceUID等(DCM_QueryRetrieveLevel元素必須添加,有時候會誤認爲添加了AffectedSOPClassUID後就不須要了,這是錯誤的。不然服務端會返回以下錯誤,以下圖)。
注:關於Patient、Study、Series等不一樣級別的查詢的詳細介紹可參考DICOM3.0標準第4部分的附錄C。
C-STORE與C-FIND相似,一樣須要添加額外的數據,不一樣於C-FIND添加查詢屬性列表的是,C-STORE添加的是準備發送的DCM文件的數據體,即下圖中的Data Set。
OFCondition cond = EC_Normal; T_DIMSE_Message req, rsp; DcmDataset bzero((char*)&req, sizeof(req)); bzero((char*)&rsp, sizeof(rsp)); /* set corresponding values in the request message variable */ req.CommandField = DIMSE_C_STORE_RQ; request->DataSetType = DIMSE_DATASET_PRESENT; request->req.msg.CStoreRQ = *request;
暫時咱們就只介紹C-ECHO、C-FIND和C-STORE三種服務的請求消息構造方法,其餘的相似。
fo-dicom是基於C#開發的,封裝性更強,封裝思路更傾向於按DICOM消息流來進行,即fo-dicom庫開發者在實現了整個DIMSE消息流框架的基礎上,經過給用戶預留各階段的接口來方便用戶定製本身的實現。對於DIMSE消息流框架的封裝在DicomService.cs文件中(同時也有相似於DCMTK中的ASC_方面的封裝,主要指的是A-ASSOCIATE服務及協議,在DICOM3.0第8部分有詳細介紹),對於網絡底層的封裝放在DicomServer.cs文件中(等同於DCMTK中的DUL_層)。
DICOM Message消息的基類在DicomMessage.cs文件中,而後根據請求和應答派生了兩個基類DicomRequest和DicomResponse。從fo-dicom庫的封裝以及fo-dicom對於Dataset的設計能夠看出Command和Dataset都是數據集合,不一樣的是二者存儲的元素類型不一樣。
在fo-dicom庫中構造各種消息很方便,可謂是「傻瓜式」操做,詳情以下:
DicomCEchoRequest cechoRQ=new DicomCEchoRequest();
一行代碼就順利的構建了一個C-ECHO-RQ請求指令。分析源碼可知DicomCEchoRequest繼承自DicomRequest,DicomReqeust繼承自DicomMessage。逐級查看各種的構造函數能夠發現。雖然咱們調用的是DicomCEchoRequest的默認構造函數,可是在相繼調用了基類DicomRequest(DicomCommandField.CEchoRequest, DicomUID.Verification, priority)和DicomMessage()後,順利的完成了對C-ECHO-RQ指令中各個參數構造,其中DicomMessage中構造了空的Command Set和DataSet,DicomRequest中對MessageID、Priority、SOPClassUID以及CommandFieldType進行了賦值,這簡直是太容易啦,不過也正由於此,剛入手的時候可能不知道如何來定製化本身的請求,覺得fo-dicom庫留給咱們的可操做性太少,其實否則,繼續往下看。
DicomCFindRequest cfind=DicomCFindRequest.CreateStudyQuery(patientId:」12345」); cfind.OnResponseReceived=(rq,rsp)=> { //接收到C-FIND-RSP響應消息後,本機C-FIND SCU進行的操做 //例如能夠輸出到屏幕或其餘窗口 Console.WriteLine("PatientAge:{0} PatientName:{1}", rsp.Dataset.Get<string>(DicomTag.PatientAge), rsp.Dataset.Get<string>(DicomTag.PatientName)); }
經過對比fo-dicom與DCMTK中C-FIND的構造,是否是以爲很容易。可是越容易學習和上手的東西,假若不掌握其本質越容易忘。查看DicomCFindRequest.cs源碼,能夠發現CreateStudyQuery函數已經幫助咱們添加了Study查詢級別所需的全部字段,也就是上文中提到的Identifier參數部分。代碼以下:
那麼若是咱們想像DCMTK那樣自由添加字段怎麼辦?例如在已知服務端是本身定製實現的基礎上來查詢咱們的私有字段。很簡單直接覆蓋一下CreateStudyQuery函數便可。另外fo-dicom還有一個比價便利的地方是將每種消息的回調函數直接綁定到消息中,程序寫起來比較方便,邏輯上更清晰。
DicomCStoreRequest cstore=new DicomCStoreRequest(@」c:\\test4.dcm」);
在DicomCStoreRequest一級只須要數據要發送的dcm文件名(全路徑名),一樣經過逐級來完成CommandSet和Dataset的賦值。基本流程以下:
/// <summary> /// Initializes DICOM C-Store request to be sent to SCP. /// </summary> /// <param name="file">DICOM file to be sent</param> /// <param name="priority">Priority of request</param> public DicomCStoreRequest(DicomFile file, DicomPriority priority = DicomPriority.Medium) : base(DicomCommandField.CStoreRequest, file.Dataset.Get<DicomUID>(DicomTag.SOPClassUID), priority) { File = file; Dataset = file.Dataset; SOPInstanceUID = File.Dataset.Get<DicomUID>(DicomTag.SOPInstanceUID); } <span style="white-space:pre"> </span>//DicomRequest.cs文件 protected DicomRequest(DicomCommandField type, DicomUID affectedClassUid, DicomPriority priority) : base() { Type = type; SOPClassUID = affectedClassUid; MessageID = GetNextMessageID(); Priority = priority; Dataset = null; } <span style="white-space:pre"> </span>//DicomMessage.cs文件 public DicomMessage() { Command = new DicomDataset(); Dataset = null; }
mDCM庫與fo-dicom庫實際上是相同的,只不過fo-dicom利用了最新的C#技術來重構mDCM。如博文http://blog.csdn.net/zssureqh/article/details/39621533中給出的mDCM庫的繼承圖所示,在頂層基類DcmNetworkBase中實現了DIMSE消息流的基本框架,而後按照Client和Server進行了兩路派生。mDCM的封裝有點處於DCMTK和fo-dicom之間的情況,既未作到像DCMTK那樣徹底提供各個層面底層操做函數,也沒有像fo-dicom那樣更抽象的封裝。
下面來看一下mDCM對各類消息的構造:
//DcmAssociation assoction;//已經順利創建的DICOM對等實體間的鏈接 byte pcid=associate.FindAbstractSyntax(DicomUID.VerificationSOPClass; SenCEchoRequest(pcid,NextMessageID(),Priority);
mDCM比較特殊,對於DIMSE-C服務請求的參數賦值流程與fo-dicom相似,大多參數賦值都在基類中完成,例如DcmClientBase中完成了MaxPDU、Priority,DicomClient完成CallingAE和CalledAE等;而對於總體請求消息的拼接卻又相似DCMTK,在SendCEchoRequest函數內部調用CreateRequest來完成。
byte pcid = Associate.FindAbstractSyntax(FindSopClassUID); if (Associate.GetPresentationContextResult(pcid) == DcmPresContextResult.Accept) { DcmDataset dataset = query.ToDataset(Associate.GetAcceptedTransferSyntax(pcid)); SendCFindRequest(pcid, NextMessageID(), Priority, dataset);
在query.ToDataset函數內部完成了查詢級別QueryRetrieveLevel的賦值,另外須要注意的是此時在ToDataset函數內部調用了一個虛函數AdditonalMembers用於方便派生添加自已要查詢的Identifier元素。最終仍是在SendCFindRequest函數內部利用CreateRequest建立C-FIND-RQ消息(在mDCM中的類型是DcmCommand)。
internal void SendCStoreRequest(byte pcid, DicomUID instUid, Stream stream) { SendCStoreRequest(pcid, NextMessageID(), instUid, Priority, stream); } internal void SendCStoreRequest(byte pcid, DicomUID instUid, DcmDataset dataset) { SendCStoreRequest(pcid, NextMessageID(), instUid, Priority, dataset); }
在CStoreClient類內部經過Load來載入dcm文件,提取DcmDataset數據體,而後調用SendCStoreRequest來發送C-STORE-RQ請求(DicomCStoreClient中有兩種類型的SendCStoreRequest,一種是發送DcmDataset類型數據,一種是發送Stream類型數據)。
經過對比分析三種開源庫對DIMSE-C服務消息的構造方式,能夠更清晰的瞭解DCMTK、fo-dicom、mDCM三者各自的優點。若是想了解DICOM協議的細節及底部代碼的具體實現,天然DCMTK是首選,其按照Dicom Upper Layer、A-ASSOCIATE、DIMSE三層來劃分的結構更方便咱們研究DICOM網絡傳輸的機制。而且DCMTK最新的3.6.1版本也逐漸開始按服務來對DUL_、ASC_、DIMSE_三類函數進行封裝,已經實現了C-ECHO、C-STORE服務,即DcmSCU/DcmSCP和DcmStorageSCU/DcmStorageSCP。若是想快速入手,實現DICOM的相關服務,fo-dicom天然是首選,想必這對於C#程序員來講垂手可得(mDCM能夠看作是DCMTK與fo-dicom的中間地帶)。
百度網盤:http://pan.baidu.com/s/1jGvaSr8
fo-dicom搭建簡單的DICOM Server
時間:2014-12-06
前段時間着重從dcmtk和fo-dicom(mDCM)源碼角度進行剖析,指望加深對DICOM協議的理解。知其然,知其因此然。若是「因此然」很很差懂,那咱們仍是先多多「知其然」吧。搞清楚原理的目的不也是爲了更好的運用於實踐麼?因此理論和實踐應該彼此交錯進行,理論搞不動了就搞搞應用,應用久了就鑽研鑽研理論。
之前上DCMTK官網僅僅是瀏覽關於開源庫中各個類的設計模式、依賴關係。最近在打開DCMTK官網的wiki時,才發現OFFIS對DCMTK的介紹是如此的詳細。正值國慶假日,就不深挖DCMTK源碼了,那就按照DCMTK wiki中給出的介紹來實際體驗分析一下DCMTK,從實踐角度來學習一下。
前幾篇博文分別介紹了worklist查詢服務(DICOM醫學圖像處理:基於DCMTK工具包學習和分析worklist、DICOM醫學圖像處理:利用fo-dicom發送C-Find查詢Worklist)、C-STORE服務(DICOM醫學圖像處理:storescp.exe與storescu.exe源碼剖析,學習C-STORE請求、DICOM醫學圖形處理:storescp.exe與storescu.exe源碼剖析,學習C-STORE請求(續))和C-MOVE服務(DICOM醫學圖像處理:AETitle在C-FIND和C-MOVE請求中的設置問題)。這次參考wiki中的說明利用DCMTK中的工具來說解一下如何調試PACS系統。
下文中會用到的工具備如下兩類
服務端 |
dcmqrscp |
客戶端 |
echoscu、storescu、findscu、movescu |
PACS是什麼?在DICOM標準中並無明確的定義,DICOM協議大可能是經過定義SOP來描述相關網絡服務。可是幾乎每個PACS系統會包含如下幾種SOP類,
Verification SOP Class |
又稱爲DICOM ECHO服務,用於查明網絡對端系統(即PACS)是否符合DICOM標準(即talks DICOM),以便雙方按照DICOM標準進行對話。 |
Storage SOP Classes |
將一個或多個DICOM對象存儲到PACS服務器。一個PACS系統每每須要支持多種Storage SOP Classes,用以存儲不一樣設備的圖像數據(如CT、US、MR等)。 |
Query SOP Classes |
根據指定的關鍵字查詢PACS數據庫。可是並不下載圖像,僅僅是查詢圖像有關的信息。 |
Retrieve SOP Classes |
根據Query SOP Classes的結果找到目標圖像後,利用Retrieve SOP Classes服務從PACS服務器下載圖像到本地。 |
Storage Commitment SOP Classes |
客戶經過該服務確認PACS服務端已經成功完成了圖像的歸檔。 |
所以能夠簡單的理解爲PACS就是提供了上述多種服務的服務端。在DCMTK工具包中給咱們提供了一個PACS模擬工具——dcmqrscp,該工具提供了上表中的全部服務(Storage Commitment SOP Classes除外,該部分並未包含在DCMTK開源包中,而須要購買商用版本)。
下面就利用dcmqrscp與其餘的dcmtk工具來模擬調試一下客戶端與PACS服務端的交互過程,從實際應用的角度熟悉DICOM3.0標準。
利用DCMTK給出的dcmqrscp工具包結合本身定製的配置文件來搭建咱們的PACS服務器(爲了更好的學習DCMTK工具包,不建議直接使用wiki中給出的公用版PACS,即www.dicomserver.co.uk/)
dcmqrscp跟其餘dcmtk工具包同樣,能夠經過添加-h或--help命令行參數來查看工具包的使用說明。惟一不一樣的是要想啓動PACS服務器還須要指定一個配置文件。DCMTK提供的默認的配置文件爲dmqrscp.cfg。打開dcmtk工具包中的dcmqrscp.cfg文件,其中的註釋已經很清楚。簡單歸納爲三部分:
第一部分,網絡配置,即傳統網絡編程中用到參數。如NetworkTCPPort——監聽端口,用於監聽來自客戶端的各類鏈接請求(須要注意的是要配置本身的防火牆,開放指定的端口);MaxAssociations——容許的最大鏈接數;MaxPDUSize——定義PDU傳輸時刻的最大長度等等。
第二部分,關於鏈接到dcmqrscp服務器的客戶機定義。該部分包含在dcmqrscp.cfg配置文件HostTable BEGIN和Host Table END內。默認的定義以下:
簡而言之,該部分就是定義可能鏈接到PACS服務器的客戶機信息,一般包含AETitle、HostName、PortNamer三部分。須要指出的是目前HostName(主機名稱)還不支持直接IP地址的方式,所以在本地配置的時候要格外注意。
本地機的配置以下:
acme1 = (ACME1,localhost,11110)
acme2 = (ACME2,localhost,11110)
acmeCTcompany =acme1 , acme2
第三部分,客戶機的詳細信息。該部分目的可能是爲了方便用戶的閱讀,方便配置時使用。在下文中的調試過程當中並未用到,所以就不作介紹了。
第四部分,PACS服務端存儲位置信息定義。經過該部分設置,能夠實現將不一樣客戶端傳統過來的數據歸檔到不一樣的PACS服務器目錄。同時針對不一樣的AE指定不一樣的讀寫權限、存儲的研究(study)數量等。默認的配置文件以下,
該部分配置的時候要注意路徑必須在本地已經存在,不然會引起錯誤。例如我在本地的配置以下,
ACME_STORED:\DcmScuScp\DcmScp RW (9, 1024mb) acmeCTcompany
下面給出我在本地機的dcmqrscp.cfg配置文件,
在命令行啓動dcmqrscp工具,輸出狀態以下:
PACS能夠簡單的理解爲提供了多種DICOM標準中SOP服務的軟件。咱們已經利用dcmqrscp工具啓動了一個PACS系統,接下來就按照上一節中PACS提供的SOP服務類表格來依次進行測試
VerificationSOPClass服務是每個PACS系統必須提供的一項服務,用於指出該PACS服務符合DICOM協議。DCMTK工具包中的echoscu工具可發起該請求,具體指令以下:echoscu.exe –dlocalhost 11110
11110對應於dcmqrscp.cfg配置文件第一部分給出的NetworkTCPPort,-d是調試選項,方便咱們觀察工具包的運行狀態。運行後的輸出結果以下:
喔?居然出現了幾個致命錯誤。幸虧咱們開啓了-d調試開關,從調試結果中看出錯誤的緣由是沒法識別Called AE Title,由於咱們在echoscu命令行中並未指定dcmqrscp的名稱。修改後指令以下,echoscu.exe –d localhost 11110 –aec ACME_STORE
居然又出現了一樣的錯誤?想必不少第一次接觸dcmtk的同窗看到這個結果就已經心涼了一半,無意繼續下去了。DMCTK的wiki中指出這個錯誤提示並未指出真正的錯誤緣由,這個是dcmqrscp.exe工具包的問題。這裏應該是要求咱們同時指定咱們本身的AE名稱,再次修改後的代碼以下:echoscu.exe –dlocalhost 11110 –aec ACME_STORE –aet ACME1
好吧,又出現了一樣的錯誤,我是服了。看來想好好學習應用也不是很容易的啊。爲了可以繼續後續的其餘測試,查看一下dcmqrscp工具包的源碼文件dcmqrscp.cc,找出產生上述問題的緣由。
問題排查:
在dcmqrscp.cc文件main函數中的waitForAssociation一行插入斷點,進行單步調試。如上在命令行開啓echoscu,發送C-ECHO請求。逐行運行代碼,具體流程以下,
最後代碼停留在_stricmp(HostName,CNF_Config.AEEntries[i].Peers[j].HostName)一行,以下:
該行中的HostName函數指的是咱們主機的名稱,例如我本機的名稱是:PC-201408122158,而CNF_Config.AEEntries[i].Peers[j].HostName指的是咱們配置文件dcmqrscp.cfg中的HostTable部分,其中HostName對應的就是上面配置文件中的localhost。
至此,通過簡單的源碼分析,已經順利找到了問題的緣由。之因此一直提示「Called AE Title Not Recognized」就是由於咱們將HostTable中的Hostname誤認爲是本機IP地址的字符名稱,因此錯誤的將主機名稱設置成了localhost。其實在dcmqrscp工具包的配置文件dcmqrscp.cfg中曾有過提示「Note:in the current implementation you cannot substitutean IP address for a hostname」。
從新修改配置文件中的hostname爲PC-201408122158,再次進行嘗試。這次鏈接測試順利經過,測試結果以下:
鏈接測試順利經過後,利用storescu.exe工具包對Storage SOPClass服務進行測試。具體指令以下:storescu.exe –dlocalhost 11110 00.dcm –aec ACME_STORE –aet ACME1,測試結果顯示爲Success(以下圖)
通過這次storescu測試,成功的將D盤根目錄下的00.dcm文件上傳到了dcmqrscp.cfg中指定的存儲目錄下,即D:\DcmScuScp\DcmScp,在存儲的過程當中dcmqrscp將00.dcm文件進行了重命名(關於重命名的規則可參見DCMTK對dcmqrscp的wiki介紹,也能夠經過命令行參數來設定重命名的方式),同時在歸檔目錄下生成了一個名稱爲index.dat的記錄文件,以下圖:
繼續咱們的調試,經過使用storescu.exe已經可以順利的將咱們的圖像上傳到指定的PACS服務目錄下。接下來對咱們上傳的數據進行查詢測試,使用的工具是findscu.exe,測試的具體指令爲:findscu.exe -v -S -aec ACME_STORE -aet ACME1 localhost11110 -k QueryRetrieveLevel=STUDY -k StudyDate -k StudyDescription -kStudyInstanceUID,查詢的結果以下:
查詢反饋的結果與咱們在利用dcmdump工具顯示的信息徹底一致,這說明Query SOPClasses測試順利經過。
進行咱們最後一項測試,就是將上傳到PACS服務器的圖像數據從新下載到本地。測試的工具是movescu.exe,具體指令以下:movescu.exe -v -S-aec ACME_STORE -aet ACME1 -aem ACME1 --port 11110 -od D:\DcmScuScp\DcmSculocalhost 11110 -k QueryRetrieveLevel=STUDY -k StudyInstanceUID = 2.16.840.114421.81295.9407241257
本來覺得會順利將圖像保存到本地D:\DcmScuScp\DcmScu目錄下,結果PACS服務端和客戶端同時停在了以下狀態,
由上圖能夠看出網絡鏈接部分的交互已經完成了,並且對於dcmqrscp.exe模擬的PACS服務端的各項服務(VerificationSOPClass、StorageSOPClass、QuerySOPClass、RetrieveSOPClass)咱們都已經測試過了,爲什麼信息交互停留在了ConstructionAssociation RQ PDU部分呢?仔細檢查一下命令行參數以及dcmqrscp.cfg配置文件,發如今本地測試的時候咱們將dcmqrscp.exe模擬的PACS服務器監聽端口和可能連入的客戶端端口都設置成了11110,所以在進行圖像傳輸的過程當中會發生衝突,爲了驗證咱們的猜想,將ACME1客戶端的端口修改成12345,再一次進行movescu的測試,指令以下:movescu.exe -v -S-aec ACME_STORE -aet ACME1 -aem ACME1 --port 12345 -od D:\DcmScuScp\DcmScu localhost11110 -k QueryRetrieveLevel=STUDY -kStudyInstanceUID=2.16.840.114421.81295.9407241257,測試結果以下:
打開本地的D:\DcmScuScp\DcmScu目錄,能夠看到由storescu.exe上傳到PACS服務器的文件。
至此利用DCMTK工具對PACS的調試工做所有結束了,上述的調試徹底參照DCMTK中wiki的相關內容,原文連接爲http://support.dcmtk.org/redmine/projects/dcmtk/wiki/Howto_PACSDebuggingWithDCMTK,這裏個人操做僅供你們參考,若是有精力還但願仔細閱讀一下英文原文,原文的講解更詳細更全面。
後續專欄博文介紹
Dicom中的MPPS服務介紹
C#的異步編程模式在fo-dicom中的應用
VMWare三種網絡鏈接模式的實際測試
時間:2014-10-03
上篇博文爲引子,介紹了一款神奇的開源PACS系統——Orthanc。本篇開始解讀官方Cookbook中的相關內容,對於簡單的瀏覽、訪問和上傳請閱讀前篇博文。在常規的PACS系統中還未出現對於DCM圖像的修改和匿名化操做,所以這次重點介紹Orthanc利用REST API實現對DCM醫學圖像的修改(modification)和匿名化(anonymization)。對於官方Cookbook中的實例進行示範和調試,經過Orthanc源碼分析確保示例在本機良好運行。注意:官方Cookbook中的示例在Windows下會有錯誤,詳情見博文。
取名爲Orthanc源自於J.R.R. Tolkien’s(托爾金)的小說。Orthanc是艾辛格(Isengard)要塞中的黑塔,初建於第二世紀,用於儲存收納南方王國的真知晶石——palantíri,一種圓形且可以看見遠方的石頭,透過palantíri能夠跟遠方使用palantíri的人進行交流。Orthanc Server正是取palantíri的此層含義,設計出一種可在整個醫院DICOM拓撲網絡中便捷、透明以及可編程訪問醫學圖像的系統(可參照wiki百科的介紹:http://en.wikipedia.org/wiki/Isengard)。
另外,Orthanc中同時包含了「RTH「,即Radiotherapy。其實Orthanc自己源自於法國de Liège大學中心醫院(Centre Hospitalier Universitaire)對於放射治療服務的研究。
Orthanc從0.5.0版本以後引入了對DICOM資源的匿名化操做,可對患者(patients)、檢查(studies)、序列(series)和圖像(instances)多個級別進行匿名化處理。爲了方便示範,此處以instances級別爲例進行介紹:
1)按照上篇博文上傳兩幅測試圖像到Orthanc Server,以下圖所示:
2)利用curl命令行查看一下上述兩個instances,獲取ID號,結果以下:
此處獲取的instance ID號爲:c77324ec-f5e76fc5-c96846bf-2ed4097d-86f9e79c
3)按照Orthanc官方Cookbook的說明,進行匿名化操做
輸入指令:curl http://localhost:8042/instances/c77324ec-f5e76fc5-c96846bf-2ed4097d-86f9e79c/anonymize -X POST -d '{}' > c:\orthanc-anonymize.dcm
可是並未得到如期結果,打開c盤發現orthanc-anonymization.dcm文件大小爲0KB。開啓curl的verbose模式,
curl –v http://localhost:8042/instances/c77324ec-f5e76fc5-c96846bf-2ed4097d-86f9e79c/anonymize -X POST -d '{}' > c:\orthanc-anonymize.dcm
查看輸出信息以下:
發現Orthanc Server中HTTP 服務返回值爲404。
經過本身查看官方Cookbook給出的指令,除了對應instance的UID不一樣外,並未找到問題,暫且跳過,嘗試一下Modification。
1)上傳DCM文件到Orthanc Server,同Anonymization中相同;
2)利用curl http://localhost:8042/instances獲取指定instance的ID號;
3)參照官方Cookbook進行Modification處理
輸入以下指令,這次爲了方便,直接使用curl的verbose模式:
curl –v http://localhost:8042/instances/c77324ec-f5e76fc5-c96846bf-2ed4097d-86f9e79c/modify –X POST –d ‘{"Replace」:{"PatientName」:"hello」,"PatientID」:"world」}}' >c:\orthanc-modify.dcm
輸出結果以下:
目前爲止,參照Orthanc官方的Cookbook說明,給出的兩個示例都在本機沒法順利完成。所以猜想官網中給出的代碼有多是Linux系統的,在Windows系統下應該作出適當的調整,可是該怎樣調整呢?該調整哪一部分呢?請繼續往下看……
在調試以前,咱們先要搞清楚出現問題的大體緣由,排除一些常見的錯誤,好比軟件版本錯誤、指令拼寫錯誤等等。上面介紹的Anonymization和Modification都是利用了Orthanc的REST API功能,那麼Orthanc各版本對於REST API的支持程度如何呢?咱們查看一下官方的說明,以下圖所示:
從上圖能夠看出,Orthanc從0.5.0版本以後就支持多級別的修改和匿名化操做,如上一篇博文所述,我本機安裝的是最新的0.8.5版本。所以能夠確保軟件版本無誤,另外在上述示例仿真過程當中咱們也對比排查了指令拼寫錯誤。
接下來使出咱們的殺手鐗吧:「啓動C:\Orthanc-0.8.5\Orthanc.sln工程,進入調試模式,查看Orthanc的源碼」。打開Orthanc.sln解決方案,右鍵Orthanc工程開啓調試模式,在關於REST API的幾個核心類中插入斷點,初次嘗試插入的斷點以下:
從新按照上一節中的步驟,首先利用Orthanc Explorer向調試模式下的Orthanc Server添加DICOM文件,此時直接按F5跳過調試,由於圖像加載過程當中咱們並未遇到問題。(注:此步操做必須從新添加,由於調試模式下數據的存儲目錄是C:\Orthanc-0.8.5\OrthancStorage,與二進制安裝包默認的C:\OrthancStorage不一樣,上一節中咱們添加的圖像在調試狀態下是看不到的)
輸入指令查看ID:
curl http://localhost:8042/instances
開始輸入上一節的Anonymization指令:
curl -v http://localhost:8042/instances/c77324ec-f5e76fc5-c96846bf-2ed4097d-86f9e79c/anonymize -X POST -d '{}' > c:\orthanc-anonymize.dcm
Orthanc工程首次停在了斷點RestApi.cpp中的Visit函數內,以下圖所示:
利用F11單步調試,隨後進入到解析Anonymization請求操做的函數ParseAnonymizationRequest內部,能夠看到在該函數內給出了一個示例與咱們輸入的格式相同
繼續單步調試,最後發如今解析Json格式的AnonymizationRequest指令,即咱們輸入的'{}’,json_reader.cpp中的readToken返回值爲tokenError。
至此想必咱們找到了Anonymization指令出錯的緣由的:輸入的Json格式不正確,雖然咱們是按照Orthanc官方的Cookbook來進行的。爲了肯定Json格式是否有誤,在在線Json格式檢查網站(http://www.bejson.com/)測試一下,結果爲:
按照提示,去掉指令中Json部分的單引號,輸入:
curl -v http://localhost:8042/instances/c77324ec-f5e76fc5-c96846bf-2ed4097d-86f9e79c/anonymize -X POST -d {} > c:\orthanc-anonymize.dcm
curl調試輸出的結果爲:
利用DICOM看圖軟件,打開能夠發現orthanc-anonymize.dcm文件中的患者姓名已經被隱藏掉了。
【總結】:正確的指令應該是外邊不添加單引號,以下所示:
curl -v http://localhost:8042/instances/c77324ec-f5e76fc5-c96846bf-2ed4097d-86f9e79c/anonymize -X POST -d {} > c:\orthanc-anonymize.dcm
既然已經找到了Anonymization中代碼出錯的問題,讓咱們去掉Modification中的單引號,嘗試一下輸入以下指令:
curl -v http://localhost:8042/instances/c77324ec-f5e76fc5-c96846bf-2ed4097d-86f9e79c/modify -X POST -d {"Replace":{"PatientName":"hello","PatientID":"world"}} >c:\orthanc-modify.dcm
令咱們失望的是依然看到了HTTP Server返回的404錯誤。
因爲Modification指令中單引號內部還存在着更多的雙引號。根據上次的經驗,有可能仍是Json格式輸入輸入錯誤。讓咱們只保留json_reader.cpp中的readToken斷點,直接查看一下解析結果是不是tokenError?從新輸入指令,進入到readToken內部,發現函數依然返回tokenError,結果以下圖所示:
利用VS自帶的查看工具,咱們能夠看到readToken函數解析的字符串current_,其真實內容是:{Replace:{PatientName:hello,PatientID:world}},其中並未看到咱們輸入的雙引號,將{Replace:{PatientName:hello,PatientID:world}}輸入到http://www.bejson.com/中,也獲得了錯誤的結果提示:
這說明咱們在命令行中輸入的Json指令中的雙引號,在被VS讀入過程當中被忽略了。那麼咱們利用轉義字符將雙引號傳遞進入,輸入指令:
curl -v http://localhost:8042/instances/c77324ec-f5e76fc5-c96846bf-2ed4097d-86f9e79c/modify -X POST -d {\"Replace\":{\"PatientName\":\"hello\",\"PatientID\":\"world\"}} >c:\orthanc-modify.dcm
查看結果:
最終運行輸出爲,
在DCM看圖軟件中打開orthanc-modify.dcm,能夠看到PatientID和PatientName已經修改。
至此,關於Orthanc Modification和Anonymization的調試已經順利完成,能夠鬆一口氣啦,仍是調試源代碼給力啊^_^。
Orthanc做爲開源項目,獲取其源碼天然是垂手可得。雖然號稱輕型Server系統,可是代碼量仍是足以嚇退大多數人的。初次面對海量的代碼無從下手進行調試,下面以上述調試Orthanc官方Cookbook中的Anonymization和Modification示例過程爲例,講解一下如何開始調試Orthanc。
之因此上一節最開始將斷點設置在了RestXXX.cpp文件中,是由於本博文中的介紹的Modification和Anonymization使用的是Orthanc提供的REST API服務,所以首先推測錯誤應該出如今REST API的相關實現函數中。這種方法能夠順利的解決咱們上面遇到的問題,而且如你所見咱們已經實現了問題的排查。
可是既然調試進入了Orthanc的源碼內,就順道兒搞清楚Orthanc中Http Server的總體流程,也順便找找到底是在哪個地方將curl指令中的雙引號忽略的。 再一次輸入指令,進入調試狀態。斷點第一次停在RestApi.cpp中的Visit函數處,在前一節中咱們利用「單步調試」順利的逐步進入到了JSON的解析函數readToken內,「單步調試」是向前追溯的最佳手段,也是VS提供給咱們的最直觀最有利的錯誤排查工具。可是爲了要了解整個流程,咱們此處須要「回溯」,那麼VS提供給咱們逆流而上的工具是什麼呢?那就是「查找全部引用」,以下圖所示:
咱們能夠由RestApi.cpp 轉到上一級,即RestApiHierarchy.cpp中的LookupResource函數,如此迭代下去相信能夠回溯到整個流程的最頂端。這種方法雖然笨拙了一些,可是比較實用。想必在Windows編程環境下你們仍是更喜歡「可視化」的直觀操做,即所見即所得。那麼單擊菜單欄中的「調試」,選擇「窗口」中的「調用堆棧」,能夠直觀的看到在RestApi.cpp中的Visit函數以前的各類函數調用順序,以下圖所示:
至此咱們能夠看到整個Http Server服務的起始端是mongoose.cpp中的worker_thread函數(在「調用堆棧」中相關函數右鍵選擇「轉到源碼」能夠查看函數的具體位置)。由上圖中「調用堆棧」的順序咱們已經能夠清晰的看到Orthanc Http Server的整個調用流程。那麼咱們在worker_thread函數中插入斷點,從新輸入curl指令,查看一下在整個流程的起始狀態下,Orthanc Http Server接收到的數據體是什麼格式?以下圖所示:
在process_new_connection函數內部讀取curl發出的POST請求指令時,鏈接緩存buf的內容中已經不存在雙引號了,所以能夠肯定curl的-d參數在發送JSON格式數據到Orthanc的Http Server時忽略了雙引號。後續能夠研究一下curl在windows環境下的使用,尤爲是-d參數的設置。
因爲DICOM Server比較熟悉,先驗知識比較多,所以調試起來比HTTP Server容易。在Orthanc的main函數中咱們能夠看到httpServer.Start()和dicomServer.Start(),dicomServer.Start()就是DICOM Server的入口點,因爲Orthanc是基於DCMTK開發的,所以後續的流程應該是交由DCMTK來完成的,更詳細的細節可參考本專欄前面的DCMTK網絡系列文章。預告一下下篇博文會詳細介紹利用fo-dicom開源庫打造一個簡單的DICOM Server服務端,那時再詳細的對比分析Orthanc中的DICOM Server,本篇暫且到此爲止。
https://code.google.com/p/orthanc/wiki/OrthancCookbook,Cookbook官網連接
https://code.google.com/p/orthanc/wiki/Anonymization,Orthanc的Modification和Anonymization介紹
http://curl.haxx.se/docs/manpage.html,curl中參數介紹
fo-dicom搭建簡單的DICOM Server
時間:2014-11-29
上週經過單步調試,找出了開源庫mDCM與DCMTK在對DICOM圖像進行JPEG無損壓縮時的細小區別,並順利實現了在C++和C#環境下對DICOM圖像的壓縮。可是問題接踵而至啊,隨着項目的深刻,發如今單獨的測試工程中能夠實現的mDCM版本,在嵌入到項目總體中後,卻意外地出現了錯誤,並未順利實現DICOM圖像的JPEG無損壓縮。所以須要繼續詳細對比分析mDCM與DCMTK二者,指望尋找緣由。
開啓項目的日誌功能後,獲得的信息反饋爲:
No registered codec for transfer syntax! 在 Dicom.Data.DcmDataset.ChangeTransferSyntax(DicomTransferSyntax newTransferSyntax, DcmCodecParameters parameters),在…………………………處。
從日誌獲得的反饋來看,應該是JPEG的編碼器註冊失敗。而編碼器部分包含在mDCM開源庫的Dicom.Codec64.dll程序集中。所以單步調試進入,查看工程是否順利加載了Dicom.Codec64.dll模塊。
首先單步進入的是上週測試用的獨立工程JpegLossLess,經過在Program.cs中調用Dicom.Codec.DicomCodec.RegisterCodecs();使得程序進入到DicomCodec.cs文件,程序運行到靜態類DicomCodec的靜態方法RegisterCodecs內。
如上圖所示,方法RegisterCodecs內部經過C#的程序集的動態加載和反射技術,順利識別了工程引用中添加的Dicom.dl程序集和Dicom.Codec64.dll程序集。
接下來單步調試到總體工程中,程序從主框架轉移到咱們手動添加的調用Dicom.Codec.DicomCodec.RegisterCodecs();函數處,以下圖所示:
通過幾回的調試發現,使用RegisterCodecs函數並未順利的註冊JPEG編碼器,識別出的17個程序集中只有Dicom.dll模塊。經過瀏覽DicomCodec.cs文件源碼發現,RegisterCodecs函數是靜態類DicomCodec的靜態函數,該函數實現的是自動註冊JPEG全部編碼器。繼續瀏覽發現,靜態類DicomCodec還有相似的其它函數,如public static void RegisterCodec(DicomTransferSyntax ts, Type type);和public static void RegisterExternalCodecs(string path, string pattern);兩個函數,分別是註冊指定傳輸語義的解碼器和註冊指定路徑下的程序集中的解碼器。因爲咱們利用RegisterCodecs函數並未實現自動加載JPEG解碼器的功能,並且工程中已經添加引用了DicomCodec64.dll程序集,而且在調試時刻VS2012的模塊窗口已經顯示順利加載了DicomCodec64.dll程序集。因此此時決定嘗試手動加載DicomCodec64.dll程序集,即用下面的代碼替換本來的Dicom.Codec.DicomCodec.RegisterCodecs();語句,
string path = System.IO.Directory.GetCurrentDirectory();
string pattern = "Dicom.Codec64.dll";
DicomCodec.RegisterExternalCodecs(path, pattern);
此刻單步調試能夠看到,已經成功的實現了DicomCodec64.dll程序集中JPEG解碼器的註冊,完成了將DICOM圖像JPEG壓縮的功能與總體工程的整合。
經過對比上述的自動和手動的註冊代碼,發現二者的最終都是利用的GetExportedTypes函數來完成註冊,具體代碼都是Type[] types = asm.GetExportedTypes();來提取相應的解碼器,惟一不一樣的是自動註冊中是利用AssemblyName[] referenced = main.GetReferencedAssemblies();提取該模塊的引用程序集,而手動註冊是利用的Assembly.LoadFile函數加載手動指定的程序集文件,難道是GetReferencedAssemblies函數出現了問題?GetReferencedAssemblies函數到底能不能返回咱們工程中全部的引用程序集呢?
在MSDN搜索一下GetReferencedAssemblies函數的功能,描述爲:Gets the AssemblyName objects for all the assemblies referenced by this assembly.
乍一看,好像該函數是能夠返回咱們工程中全部加載的程序集的名稱。可是仔細分析一下,描述中提到的是"this assembly」,此處this 應該指的是調用RetReferencedAssemblies函數的程序集,所以該函數應該得到的是當前模塊所引用的全部程序集,而並非咱們起初認爲的整個工程的引用程序集。通過漫長的搜索,終於在一篇stackoverflow的博文(http://stackoverflow.com/questions/3971793/what-when-assembly-getreferencedassemblies-returns-exe-dependency)中找到了對「提取工程全部依賴程序集」的相關說明,文中做者不只給出了實現的方法,並且給出了爲何GetReferencedAssemblies函數沒有返回工程全部引用程序集的緣由(http://msdn.microsoft.com/en-us/magazine/cc163641.aspx)。此處簡單的對其概括一下,並借用一下原做者的圖:
以下圖所示,假設咱們在模塊A中調用了GetReferencedAssemblies函數,那麼按照MSDN中對應的解釋,函數應該返回this——即A所引用(更確切的說是直接應用)的程序集B、C、D。然而以下圖左所示,程序集C和D又分別引用了其餘的程序集,因此此處咱們並未直接獲取到整個工程中全部的程序集。所以自動加載的時候並未順利的返回咱們須要的Dicom.Codec64程序集。
說到這裏,我想提取工程全部引用程序集的方法已經呼之欲出了,最簡單的就是咱們能夠對GetReferencedAssemblies的首次返回值進行遞歸調用,那麼天然而然就能夠獲得全部的引用程序集A-J。可是博文中做者是按照上圖右中的方式來提取全部引用程序集的,由於遞歸會影響程序的性能,尤爲是程序模塊衆多的時候。簡言之,就是利用算法導論中的「前序遍歷」來提取全部的引用程序集,具體代碼能夠從給出的參考博文下載。
在對比mDCM與DCMTK兩個開源庫對於JPEG解碼器註冊的源代碼後,發如今用C++完成的DCMTK開源庫中,使用的是Singleton設計模式的DcmCodecList類來完成JPEG各類解碼器註冊的,而用C#編寫的mDCM開源庫使用的是C#的靜態類public static DicomCodec。這兩種方式能夠實現相同的功能,因爲剛開始從C++轉向C#,對於這二者的區別不是很清楚,所以搜索了一下,僅摘取部分重要片斷貼在博文中,便於之後查閱。
【摘要1】:http://bbs.csdn.net/topics/370008452
除了跨程序集的邊界問題,static 類和模仿 GoF C++ 版的單件沒有本質的區別。我感興趣的討論在於這二者在知足一樣的動機的狀況下,是否達成了一樣的效果,我我的的見解是,靜態類有簡單和優雅的一面。事實上,在Java和C#方面,GoF的設計模式自己有問題,這就是經典的Double Lock Check問題(看 CLR via C#)。
粗略地說,在C# 4中,這些模式消失了:單件(靜態類)、策略(委託和Lambda)、觀察者(事件)、裝飾(擴展方法)、工廠(部分靠反射實現)、代理(表達式樹和動態類)、迭代器(yield return語法),等等,若是你按照GoF的實現來作這些,你反而捨近求遠了。
最後,不光是 singleton,我對設計模式一個廣泛的見解是,隨着編程語言的進步,全部設計模式的實現都將消亡,而思想保存了下來。設計模式的本質也能夠說是爲了修飾語言的缺陷,一種優雅的語言,不須要設計模式(這個觀點是我一個大學同窗提出的,他也是一位 Ruby 社區的專家)。
【摘要2】:http://www.cnblogs.com/utopia/archive/2010/03/02/1676390.html
靜態類的語義是全局惟一代碼段,而單件的語義是全局惟一對象實例;
語義上是徹底不一樣地,不能提及修飾都是「全局惟一」就放一塊比較;
若是是這樣那麼全部public修飾的東西咱們不是都得比較一翻了;
另外:若是要研究對象設計,那麼請先拋開代碼。對象設計是自己哲學性和世界觀的表達。
如何把現實的東西用概念還原表達出來,纔是對象設計的實質。而代碼則是體現你頭腦裏那個概念模型的工具。
【摘要3】:http://blog.csdn.net/lyrebing/article/details/1902235
單例模式的目的是爲了在程序中提供類的惟一實例,並且僅提供惟一的訪問點。靜態不須要實例,僅提供一個全局功能。使用單例能夠繼承,實現接口,而靜態類不能。靜態方法不能訪問類中的實例字段,由於靜態方法不是經過實例來訪問的。而單例中的方法卻能夠訪問那個惟一實例中的實例字段。靜態方法在執行後,會釋放掉它所建立的全部對象。而單例中的方法卻能夠保留。靜態字段僅是提供全局的功能,你們共享同一內存位置。訪問單例中的字段是類的惟一實例中的字段,你們只能訪問這個實例的字段。
時間:2014-08-17
背景介紹:
醫學影像PACS工做站的服務端須要對大量的dcm文件進行歸檔,寫入數據庫處理。因爲醫學圖像的特殊性,每個患者(即所謂的Patient)每作一次檢查(即Study)都至少會產生一組圖像序列(即Series),而每一組圖像序列下會包含大量的dcm文件(例如作一次心臟CTA的診斷,完整的一個心臟斷層掃描序列大約有200幅圖像)。DICOM3.0協議中對每一幅影像是按照特定的三個UID(惟一標示符)來進行標記的,分別是StudyInstanceUID、SeriesInstanceUID、SOPInstanceUID。其中StudyInstanceUID表明了惟一的一次檢查(Study),SeriesInstanceUID表明了相應檢查下的惟一序列(Series)、SOPInstanceUID表明了惟一檢查下的惟一序列下的惟一圖像。一般PACS工做站都是利用這三個UID來對dcm文件進行歸檔處理。
歸檔的設計:
一、基本的歸檔結構是:
第一級:StudyInstanceUID |
存儲同一患者的影像數據 |
第二級:SeriesInstanceUID |
存儲同一次檢查下的影像數據 |
第三級:SOPInstanceUID |
存儲同一個序列下的影像數據 |
二、INI配置文件的生成
INI配置文件的格式就不細講了,CSDN中已有不少詳細講解的博文,請你們自行參閱。此次詳細講解一下利用dcmtk開源庫來提取相應的dcm文件信息並寫入到ini配置文件中的方法。
DCMTK開源庫是一個很好的醫學影像開發基礎庫,其很好的實現了DICOM3.0標準,且類的繼承體系簡單明瞭,與DICOM3.0標準一一對應(隨後會寫關於「dcmtk開源庫的繼承體系與DICOM3.0標準的對應關係」的博文)。這裏咱們只用到了dcmtk中的DcmItem類,該類派生自DcmObject基礎類,其含有ElementList成員變量,存儲了DICOM3.0標準中規定的一系列的數據元(Data Element)基本結構以下圖所示:
注:DcmItem類就是Dataset(數據集)的子類。其內部包含了數據元序列(即ElementList數據成員)。
經過閱讀關於DcmItem類的源碼,總結概括瞭如下幾種dcmtk開源庫給出的操做dcm文件相應數據元的函數:findAndGet 函數、findOrCreate函數、findAndXXX函數、putAndInsert函數,以及insertXXX函數,以下圖:
至此咱們能夠利用findAndGet函數類來提取dcm文件中的相關信息,結合WindowsAPI函數來進行INI配置文件的歸檔。因爲INI配置文件就是文本文件,所以咱們選用了DcmItem中的findAndGetString函數來提取dcm文件中的數據元,利用findAndGetString函數可以直接獲得字符串格式(const char*)的數據元,另外,結合WritePrivateProfileString函數來生成INI配置文件。(注:此處findAndGetString函數正好與WritePrivateProfileString函數的格式匹配,若是採用findAndGet的其餘函數,如findAndGetSin32,就須要利用itoa等函數將整型轉換成const char*類型,增長了編程的複雜性)
下面給出部分代碼:
DcmTagKey THU_DCM_ELEMENTS[]= {DCM_InstanceNubmber, DCM_Rows,DCM_Columns,DCM_PatientName};//定義須要寫入到ini文件中的dcm數據元標籤數組 int num=sizeof(THU_DCM_ELEMNTS)/sizeof(DcmTagKey); OFString mImageValue; OFString mGap("|");//INI配置文件中各個數據元之間的間隔符 OFString mImageModule("ImageModule\\");//配置文件的節名稱 OFString mSOPInstanceUID; OFString mSeriesInstanceUID; DcmDataset *pDataset=mDcmFile->getDataset(); DcmMetaInfo *pMetaInfo=mDcmFile->getMetaInfo(); pDataset->findAndGetOFString(DCM_SeriesInstanceUID,mSeriesInstanceUID); pDataset->findAndGetOFString(DCM_SOPInstanceUID,mSOPInstanceUID); mImageModule+=mSeriesInstanceUID; for(int i=0;i<num;++i) { OFString mValueRecord; DcmElement *element; if(THU_DCM_ELEMNTS[i].getGroup()>0x0002)// to determine if the THU_DCM_ELEMENTS[i] is MetaInfo { //the element belongs to Dataset pDataset->findAndGetOFStringArray(THU_DCM_ELEMNTS[i],mValueRecord); mValueRecord+=mGap; } else { //the element belongs to MetaInfo if(THU_DCM_ELEMNTS[i].getGroup()==0x0000 && THU_DCM_ELEMNTS[i].getElement()==0x0000) { mValueRecord=mGap; } else { pMetaInfo->findAndGetOFStringArray(THU_DCM_ELEMNTS[i],mValueRecord); mValueRecord+=mGap; } } mImageValue+=mValueRecord; } ::WritePrivateProfileString(mImageModule.c_str(),mSOPInstanceUID.c_str(),mImageValue.c_str(),iniFileName);
三、數據庫的寫入:
數據庫寫入的方式與INI配置文件生成基本類似,只要稍微瞭解C++數據庫編程的人員,就很容易仿照上述INI配置文件的生成過程來完成數據庫寫入的部分,此處就不細講了,只給出簡單的部分代碼:
DcmFileFormat fileformat; TCHAR FilePath[MAX_PATH]; OFCondition oc = fileformat.loadFile(FilePath); DcmDataset *pDataset=fileformat.getDataset(); char query[1000]; memset(query,0,sizeof(char)*1000); lstrcat(query,_T("insert into patient values (")); const char *tString; pDataset->findAndGetString(DCM_InstanceNumber,tString); lstrcat(query,tString); lstrcat(query,_T(",")); pDataset->findAndGetString(DCM_PatientName,tString); lstrcat(query,_T("\"")); lstrcat(query,tString); lstrcat(query,_T("\"")); lstrcat(query,_T(",")); pDataset->findAndGetString(DCM_PatientID,tString); lstrcat(query,tString); lstrcat(query,_T(",")); pDataset->findAndGetString(DCM_PatientBirthDate,tString); lstrcat(query,tString); lstrcat(query,_T(",")); pDataset->findAndGetString(DCM_PatientSex,tString); lstrcat(query,"\""); lstrcat(query,tString); lstrcat(query,"\""); lstrcat(query,","); pDataset->findAndGetString(DCM_PatientAge,tString); char temp[100]; memcpy(temp,tString,lstrlen(tString)); temp[lstrlen(tString)]=_T('\0'); for(int i=0;i<strlen(temp);++i) if(temp[i]==_T('Y')) temp[i]=_T('\0'); lstrcat(query,temp); lstrcat(query,","); lstrcat(query,_T("2013)")); //mysql數據庫的寫入 MYSQL* con; con=mysql_init((MYSQL*)0); if(con!=NULL && mysql_real_connect(con,host,user,passwd,db,port,unix_socket,client_flag)) { if(!mysql_select_db(con,db)) { ::printf("Selcet successfully the database!\n"); con->reconnect=1; int rt=mysql_real_query(mysql,query,strlen(query)); if(rt) { ::printf("Error making insert!!!\n"); } } }對於MYSQL的C++操做,能夠參見博文: http://www.cnblogs.com/justinzhang/archive/2011/09/23/2185963.html
預告了很久的幾篇專欄博文一直沒有整理好,主要緣由是早前但願搭建的WML服務器計劃遇到了問題。起初覺得參照DCMTK的官方文檔wwwapp.txt結合前兩天搭建的WAMP服務器能夠順利的實現WML服務,藉此就能夠同時完成WEB PACS系列以及搭建Dicom WML服務器的兩篇博文。但是在實際部署過程當中發現了幾個嚴重的問題,一時沒法解決。可是在搜索解決方案的時候,偶然間找到了在DCMTK論壇上貼出來的用PHP對DCMTK工具包封裝的文章。所以此篇博文在記錄搭建WML遇到的問題的同時,主要想向你們介紹一下這個簡單的封裝DCMTK工具包的PHP類,並在前期搭建的WAMP服務器上給出示範實例。(PS:也但願知道如何解決該問題的大神趕忙現身)
按照DCMTK官方文檔wwwapp.txt文件(http://support.dcmtk.org/docs/file_wwwapp.html)的說明,搭建DICOM Basic Worklist Management服務的前期準備工做已經基本完成,前述的WEB PACS平臺已經可以順利提供HTTPD、CGI以及Perl解析的功能(具體可參見博文中給出的Perl示例:http://blog.csdn.net/zssureqh/article/details/40516745)。可是按照wwwapp.txt文檔指示拷貝wwwapps目錄下的可執行文件(例如preplock、readoviw、readwlst、writwlst)時,並未在編譯後的工程中找到,只看到了相應的.cc源碼文件。
搜索相關資料後,發現柳北風兒前輩此前也遇到過該問題,並向OFFIS的相關維護人員進行過諮詢。前輩的說明博文地址是:http://qimo601.iteye.com/blog/1701026,OFFIS論壇的討論地址是:http://forum.dcmtk.org/viewtopic.php?f=1&t=723&hilit=wwwapp.txt。
按照上述的說明,確信應該是在編譯DCMTK源碼中間的某個環節出現了問題,致使本應該順利生成的幾個exe文件丟失。官方論壇中的討論是針對Linux環境下利用make工具來編譯的狀況,該環境下在利用make安裝的時候由make distclean指令來控制preplock、readoviw、readwlst、writwlst等可執行文件的清除。可是在Windows環境下用的是CMake來生成與VS對應的sln文件,打開DCMTK.sln解決方案後並未找到如何設置才能編譯生成上述可執行文件,並且按照柳前輩的說法,即便編譯成功,在Win7環境下一樣缺乏一個preplock.exe文件。至此該問題的解決就終止了,到發文時刻還未找到很好的解決方法。
在瀏覽OFFIS論壇,尋找上述問題的解決方案時,無心點開了論壇中的「Third-Party DCMTK Applications」分支,以下圖所示,該分論壇中介紹了衆多DCMTK相關的應用開發,其中有一項叫作「PHP DICOM Class「。
其中做者Vedicveko給出了PHP Dicom Class類的設計初衷以及詳細的使用說明,說明文檔網址爲:http://deanvaughan.org/wordpress/dicom-php-class/。
下載源碼(https://github.com/vedicveko/class_dicom.php/zipball/master)後,打開class_dicom.php核心類文件,能夠看出做者經過使用PHP中的exec命令來對DCMTK對應的工具包進行了封裝,藉助於PHP語言的優點使得DCMTK更易於網絡化應用。Apache網絡服務器與PHP之間的調用能夠直接利用咱們前面搭建的簡易WEB PACS平臺(該平臺對於PHP的調用經過FastCGI來實現),而後經過結合PHP DICOM Class能夠實現對dcm文件的大多數操做,具體的實現以下。
第一,將DCMTK編譯後的工具包統一放到指定位置,例如個人本機地址爲:c:\dcmtk\bin,修改class_dicom.php文件中的以下代碼,將TOOLKIT_DIR指向本機工具包目錄c:\dcmtk\bin。
define('TOOLKIT_DIR', 'C:/dcmtk/bin'); // CHANGE THIS IF YOU HAVE DCMTK INSTALLED SOMEWHERE ELSE
第二,藉助前面搭建的WAMP服務,在網站服務根目錄(我本機爲c:\wamp\www\)下新建class_dicom_php目錄,將下載的PHP DICOM Class源碼文件直接拷貝到class_dicom_php目錄下,以下圖所示:
第三,開啓wamp server服務,在瀏覽器中輸入http://localhost/class_dicom_php/examples/get_tags.php,進行測試,正常的話會輸出dean.dcm文件的Tags標籤信息,以下圖所示:
如上所示瀏覽器中看到的結果與利用dcmdump.exe工具查看的結果一致,說明PHP DICOM Class已經順利的安裝到了WEB PACS平臺中。
【注】:在實際運行過程當中可能會出現錯誤,緣由是get_tags.php中使用的是$argv命令行變量來得到具體的dcm文件路徑的,可是在WEB PACS中咱們只能經過GET或者POST方式傳遞參數到php腳本,所以可修改get_tags.php中的參數獲取方式,或者直接將測試文件dean.dcm寫入到文件路徑變量中,以下所示:
$file = (isset($argv[1]) ? $argv[1] : 'dean.dcm');
#原來代碼爲:$file = (isset($argv[1]) ? $argv[1] : ' ');
修改後再次在瀏覽器中輸入http://localhost/class_dicom_php/examples/get_tags.php,就會順利獲得上述結果。
雖然PHP DICOM Class只是簡單的調用了DCMTK工具包來實現PHP對DICOM文件的操做,可是因爲DCMTK工具包的強大,在目前咱們簡易的WEB PACS平臺的併發數不大的狀況下,能夠嘗試直接利用PHP DICOM Class來實現前篇博文中將DCM文件的圖像信息輸出到瀏覽器的功能。
查看class_dicom.php,看到其中dicom_convert類中有關於JPEG到DCM的自由雙向變換,其源碼中用到的是DCMTK工具包中的dcmj2pnm,查看dcmj2pnm的幫助文檔可知,該工具也可實現DCM到bmp文件的轉換,所以決定對class_dicom.php中的dicom_convert類進行擴展,添加dcm_to_bmp()函數,具體代碼以下:
### zssure 20141104 function dcm_to_bmp() { $filesize = 0; $this->jpg_file = $this->file . '.bmp'; $convert_cmd = BIN_DCMJ2PNM . " +ob " . "\"" . $this->file . "\" \"" . $this->jpg_file . "\""; $out = Execute($convert_cmd); if(file_exists($this->jpg_file)) { $filesize = filesize($this->jpg_file); } return($this->jpg_file); }
固然也能夠經過識別dcm具體的圖像標籤來自動設定保存的bmp格式,在自適應時刻用到的主要標籤以下圖所示:
編寫dcm_to_bmp的測試php,代碼以下:
#!/usr/bin/php <?PHP # # Creates a jpeg and jpeg thumbnail of a DICOM file # require_once('../class_dicom.php'); $file = (isset($argv[1]) ? $argv[1] : 'dean.dcm'); if(!$file) { print "USAGE: ./dcm_to_jpg.php <FILE>\n"; exit; } if(!file_exists($file)) { print "$file: does not exist\n"; exit; } $job_start = time(); $d = new dicom_convert; $d->file = $file; $d->dcm_to_bmp(); #$d->dcm_to_tn(); #system("ls -lsh $file*"); $job_end = time(); $job_time = $job_end - $job_start; #print "Created BMP and thumbnail in $job_time seconds.\n"; header("Content-type:image/bmp\n\n"); $jpgName=$d->jpg_file; $fp=fopen($jpgName,"r"); fpassthru($fp); exit; ?>
至此,利用PHP DICOM Class快速便捷地實現了將dcm文件的圖像信息輸出到瀏覽器的功能。
【注】:在上述dcm_to_bmp.php測試文件中須要將#system("ls -lsh $file*");語句註釋掉,不然在windows的WAMP環境下會出現問題。
原來只是利用OFFIS的論壇(http://forum.dcmtk.org/index.php)來搜索使用DCMTK過程當中遇到的各類錯誤,歷來沒有仔細全面的瀏覽過OFFIS論壇的各個部分,經過今天的親身經歷,發如今OFFIS論壇的DCMTK項目下的【Third-Party DCMTK Applications】部分也是一個知識寶藏,裏面包含了各類牛人利用DCMTK開發的工具,大多都是開源的,相關文檔也很詳細,之後能夠做爲重點學習的資料。
下面給出幾個我以爲很值得學習的連接,供你們參考:
利用PHP Skel結合DCMTK開發WEB PACS應用
利用DCMTK搭建WML服務器
利用oracle直接操做DICOM數據
C#的異步編程模式在fo-dicom中的應用
VMWare三種網絡鏈接模式的實際測試
時間:2014-11-04
最近兩篇專欄博文講解的都是有關WEB PACS環境的搭建,若是搭建的平臺後端不進行DICOM的相關操做,其實跟PACS壓根就一點關係也沒有,因此最近幾篇看似有些跑題,不過你們不要着急,開發環境的搭建自己就是一項巨大並且艱難的工程,等調試好環境後續的PACS相關開發就會如單機版同樣駕輕就熟,再忍耐一會,近期立刻會開始介紹在平臺上進行WEB PACS的研發。
上兩篇博文只是對該環境的一個取巧的嘗試,第一篇博文直接利用APACHE服務自帶的CGI,直接調用C編譯後的exe文件,因爲CGI技術已經逐漸被FastCGI取代,因此第一篇博文僅做爲示範來用,不是運用到後期實際開發中;第二篇博文延續第一篇,試圖進行圖像的傳輸,最終也未能找到C語言實現圖像數據傳輸的正確方法,只能偷懶的用PHP和Perl等解釋型語言來實現,可是PHP和Perl不能很好與咱們前面介紹的DCMTK融合,因此第二篇博文也沒有太大的做用。鑑於上述緣由,遂決定在已經搭建的FastCGI平臺的基礎上,添加C/C++語言編寫PHP擴展的框架,來實現最終WEB PACS的完整開發環境。
1)PHP源碼:下載PHP源碼的目的是爲了生成最新的、最符合本機的php運行程序,即php.exe(固然也能夠直接百度/谷歌最新的PHP安裝包,雙擊安裝便可);
2)PHP源碼編譯:PHP源碼是開源的,要求在linux下編譯,因此須要在windows下安裝一個mini的Linux編譯環境,即下載Cygwin或者msys+MinGWin(二者詳細的區別可參照此博文http://zengrong.net/post/1723.htm中的介紹);——記得安裝完成Cygwin(即Linux編譯器)後,修改ext_skel_win32.php中的$cygwin_path = 'c:\cygwin64\bin';語句,改成本身的路徑,目的是爲了指明sh.exe的位置。
3)PHP SDK:SDK,全稱爲Software Development Kit,顧名思義就是軟件開發過程當中經常使用的小工具(可理解爲API)的集合。此處咱們只須要使用php-binary-sdk包中的bison.exe和flex.exe,將其拷貝到當前windows目錄下——目的是爲了咱們可以找到(也可將二者的路徑添加到環境變量Path中)。
本機電腦安裝的是VS2012和VS2010,此處選擇VS2012來編譯PHP源碼。首先進入VS的命令行狀態,以下圖:
輸入:cd c:\PHPDev\php-5.6.2
轉換到php源碼目錄,該目錄下有buildconf.bat文件。(此處PHPDev是我本身創建的上一級目錄,在實際編譯過程當中請更換爲本身本地的目錄)
輸入:buildconfig.bat,該批處理命令中啓動腳本程序buildconfig.js,搜索目錄下全部的.w32文件爲windows環境下的編譯作準備。若是運行成功會提示「Now run 'configure --help'」,不然提示「Error generating configure script, configure script was not copied」。成功編譯後,按照提示可先輸入configure --help查看編譯選項,隨後參照博文中http://demon.tw/software/compile-php-on-windows.html的說明輸入:
configure –without-xml –without-wddx --without-simplexml --without-dom --without-libxml --disable-zlib --without-pdo-sqlite –-disable-odbc –-disable-cgi --enable-debug --without-iconv --disable-ipv6
注意此處略不一樣於博文中的介紹,without前是兩個「-」,具體參照configure --help的提示,以下圖:
配置完成後就是編譯過程了,輸入nmake,開始實際編譯。
【注】:編譯過程當中可能會出現錯誤,例以下圖所示,這種狀況下利用EditPlus或者NotePad++等編輯工具將文件轉存爲utf-8編碼,從新啓動nmake便可。
編譯完成後會出現Debug_TS文件夾,該文件夾下就是咱們編譯源碼後生成的php可執行文件,其中會看到php.exe文件,下圖是nmake編譯成功的結果圖。
進入到Debug_TS目錄下,輸入php.exe 「echo ‘Hello World’;"測試能夠看到正確的輸出,說明在windows平臺下編譯php源碼的工做順利完成了。
php.exe ext_skel_win32.php --extname=zsgetdcmimage
得到了PHP可執行程序後,就是骨架生成了。進入到php源碼中的ext文件夾,能夠看到用於骨架生成的兩個文件,ext_skel和ext_skel_win32.php。打開ext_skel_win32.php,大體上能夠看出如何來構建骨架程序的,其實就是經過Cgywin中提供的sh.exe工具,啓動sh.exe ext_skel,隨後將本來已經寫好的一個基於C/C++動態庫的工程經過php腳本的方式自動修改成咱們本身命名的工程。能夠打開源碼ext\skeleton目錄,發現其中存在着一個名稱爲skeleton.dsp的工程,與最終咱們得到的骨架工程僅僅是名稱不一樣而已。
基本環境搭建完成了,讓咱們來進行一次實際測試。
骨架工程中給咱們提供了一個與咱們工程名稱相同的測試函數,例如個人zsgetdcmimage工程中的函數爲:
/* Remove the following function when you have successfully modified config.m4 so that your module can be compiled into PHP, it exists only for testing purposes. */ /* Every user-visible function in PHP should document itself in the source */ /* {{{ proto string confirm_zsgetdcmimage_compiled(string arg) Return a string to confirm that the module is compiled in */ PHP_FUNCTION(confirm_zsgetdcmimage_compiled) { char *arg = NULL; int arg_len, len; char *strg; len = spprintf(&strg, 0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "zsgetdcmimage", arg); RETURN_STRINGL(strg, len, 0); }
由於上面咱們編譯的php源碼開啓的是調試狀態,即參數爲--enable-debug,因此在編寫骨架程序時刻要對應的用Debug_TS模式編譯,同時在添加連接庫爲php5ts_debug.lib,該文件在php編譯後的源碼文件夾Debug_TS中。
編譯成功後,將生成的zsgetdcmimage.dll拷貝到咱們編譯出來的php可執行程序的目錄Debug_TS中的ext子目錄下,而後修改Debug_TS中的php.ini,添加extension=ext/zsgetdcmimage.dll語句。
最後建立一個測試php文檔test.php,代碼以下:
<?php echo confirm_zsgetdcmimage_compiled("zssure"); ?>
在命令行狀態下,轉到Debug_TS目錄下,輸入php.exe test.php,若是配置成功,會獲得以下輸出:
至此骨架擴展及實例測試已經完成了。
前兩篇博文中已經利用wamp搭建了一個基於Apache+PHP+MySQL的Web服務框架,將上述編譯完成的zsgetdcmimage.dll擴展放入到c:\wamp\bin\php5.5.12\ext中,啓動wampServer後,居然沒法調用出現以下錯誤提示:
該錯誤說明利用php5.6.2擴展骨架生成的擴展放在php5.5.12下不匹配。既然如此,要想將PHP擴展骨架添加到前面搭建的WEB PACS平臺中只有兩種選擇:1)從新下載對應版本的php5.5.12的源碼,按照上述記錄從新生成骨架程序;2)更換wamp安裝包中的php。
本來對WampServer的配置就不是很瞭解,因此想借助這次機會來順便學習一下。遂決定嘗試更換WampServer安裝包中的PHP,有原來的php5.5.12替換成php5.6.2。網上搜索了一下,相關的資料很多。基本的操做思路主要參考了這兩篇博文的介紹http://www.cnblogs.com/heiing/archive/2011/11/15/2249948.html和http://pcwanli.blog.163.com/blog/static/45315611201441811572810/,上兩篇博文的介紹略顯簡單,且邏輯性不是很好。如今給出我在Win7 32bit+WampServer-64-bits-php-5-5環境下的具體實施步驟。首先將進入到c:\wamp\bin\php目錄,將php官網下載的最新的windows下的二進制安裝包解壓到該目錄下,例如php5.6.2的完整路徑爲c:\wamp\bin\php\php5.6.2,隨後修改方法及順序以下:
WampServer修改1 | wampmanager.conf | [php]標籤下替換爲新版的版本號5.6.2 [phpCli]標籤中的版本號替換爲5.6.2 |
WampServer修改2 | wampmanager.ini | 利用NotePad++或者EditPlus等文本編輯器搜索文件中的phpX.X.X統一替換爲php5.6.2; 【注意】:X.X.X部分也須要替換爲5.6.2,例如[switchPhp5.6.2]、[phpVersion]下的Caption等等。 |
PHP修改1 | php.ini | 1)將extension_dir設置爲新版的php.exe的路徑,extension_dir = "c:\wamp\bin\php\php5.6.2\ext\" 2)而後有選擇性的取消extension=XXX.dll的註釋,添加php的擴展 |
PHP修改2 | phpForApache.ini | 複製php.ini文件,改名爲phpForApache.ini |
APACEH修改 | httpd.conf | FastCGI配置模塊中php的版本號相應修改,以下: LoadModule php5_module "c:/wamp/bin/php/php5.6.2/php5apache2_4.dll" PHPIniDir c:/wamp/bin/php/php5.6.2 LoadModule fcgid_module modules/mod_fcgid.so <IfModule mod_fcgid.c> AddHandler fcgid-script .fcgi .php #php.ini的存放目錄 FcgidInitialEnv PHPRC "c:/wamp/bin/php/php5.6.2" # 設置PHP_FCGI_MAX_REQUESTS大於或等於FcgidMaxRequestsPerProcess,防止php-cgi進程在處理完全部請求前退出 FcgidInitialEnv PHP_FCGI_MAX_REQUESTS 1000 #php-cgi每一個進程的最大請求數 FcgidMaxRequestsPerProcess 1000 #php-cgi最大的進程數 FcgidMaxProcesses 5 #最大執行時間 FcgidIOTimeout 120 FcgidIdleTimeout 120 #php-cgi的路徑 FcgidWrapper "c:/wamp/bin/php/php5.6.2/php-cgi.exe" .php AddType application/x-httpd-php .php </IfModule> |
實際測試 | 1)將利用PHP骨架編譯的zsgetdcmimage.dll擴展放到php5.6.2\ext目錄下 2)在wamp\www目錄下創建測試php文件,test.php |
瀏覽器中輸入localhost\test.php 獲得以下輸出: |
至此更換WampServer安裝包中的PHP版本的工做順利完成,通過屢次努力,用三篇博文的篇幅咱們已經順利的搭建了WEB PACS開發的基礎環境。後續會開始介紹利用PHP的擴展骨架結合DCMTK來實現WEB PACS的各項功能。
利用PHP Skel結合DCMTK開發WEB PACS應用
利用DCMTK搭建WML服務器
利用oracle直接操做DICOM數據
C#的異步編程模式在fo-dicom中的應用
VMWare三種網絡鏈接模式的實際測試
時間:2014-10-31
上一篇博文簡單翻譯了Orthanc官網給出的CodeProject上「利用Orthanc Plugin SDK開發WADO插件」的博文,其中提到了Orthanc從0.8.0版本以後支持快速查詢,而本來的WADO請求須要是直接藉助於Orthanc內部的REST API逐級定位。那麼爲何以前的Orthanc必需要逐級來定位WADO請求的Instance呢?新版本中又是如何進行改進的呢?此篇博文經過分析Orthanc內嵌的SQLite數據庫,來剖析Orthanc的RESTful API機制,以及WADO服務的實現。
上一篇博文中提到的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協議規定的請求鏈接爲:
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,實現最終定位圖像的目的。
上面的實現是否是很繁瑣啊,哈哈。好在官方Plugin SDK說明博文中給出了最新版的定位方式,具體的實現可參見我上一篇博文(http://blog.csdn.net/zssureqh/article/details/41836885)。那麼爲什麼Orthanc起初須要如此繁瑣的定位圖像呢?這裏咱們先簡單的分析一下Orthanc內部是如何來標記文件的惟一性的,後續章節再詳細分析以前Orthanc模擬WADO服務爲什麼如此繁瑣。
在Orthanc源碼中有這樣一個類DicomInstanceHasher(定義在DicomInstanceHasher.h,實如今DicomInstanceHasher.cpp),其註釋中如此描述:
/** * This class implements the hashing mechanism that is used to * convert DICOM unique identifiers to Orthanc identifiers. Any * Orthanc identifier for a DICOM resource corresponds to the SHA-1 * hash of the DICOM identifiers. * \note SHA-1 hash is used because it is less sensitive to * collision attacks than MD5. <a * href="http://en.wikipedia.org/wiki/SHA-256#Comparison_of_SHA_functions">[Reference]</a> **/
從描述中咱們能夠知道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嵌入式數據庫,對數據庫的操做在工程代碼中集成,所以在使用過程當中並未能感受到數據庫的管理,這也支撐了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暫時不清楚,請看下文分析)。
Orthanc源碼中有DatabaseWrapper類,其中有以下注釋:
/** * This class manages an instance of the Orthanc SQLite database. It * translates low-level requests into SQL statements. Mutual * exclusion MUST be implemented at a higher level. **/
說明該類是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數據庫的基本結構後,進行一下實例測試。如博文(http://blog.csdn.net/zssureqh/article/details/41836885)所述,向Orthanc中添加數據有多種方式,命令行工具,REST API,以及網頁。下面咱們對Orthanc自帶的Explorer和DCMTK工具包storescu.exe進行真實數據上傳測試。
先打開Orthanc的瀏覽界面:http://localhost:8042/app/explorer.html#upload
拖拽任意圖像到瀏覽器內,單擊【Start the upload】,直到出現綠色'【Done】,代表上傳成功。
數據庫變化以下:
上述利用Orthanc內嵌的Explorer成功上傳並寫入數據庫。這次使用storescu.exe,把Orthanc當作Dicom Server查看數據寫入狀況,寫入指令以下:
storescu.exe -d localhost 4242 -aet ZSSURE -aec ORTHANC c:\test2.dcm
完成後數據庫變化以下:
上面利用兩種方式來完成了添加數據到Orthanc內嵌SQLite數據庫(還有REST API第三種方式,參見以前博文:,因爲原理與Explorer中類同就不單獨介紹了),而且觀察到了數據庫的真實變化,可是具體的字段含義此刻可能還不是很清楚,讓咱們利用REST API來讀取數據庫並嘗試分析下其中的含義。
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
curl http://localhost:8042/studies
返回結果爲,
即上述分析的Resources表中的每組的第三條記錄,也就是表中的43和47行。
curl http://localhost:8042/series
返回結果爲,
Resources表中每組記錄的第二條,表中的42和46行。
curl http://localhost:8042/instances
返回結果爲,
Resources表中每組記錄的第一條,表中的41和45行。
curl http://localhost:8042/patients/64d6f8a0-ea0ffdb2-a14d1488-4fa7879c-2d9758d8
對比前面數據庫的分析,發現大多數字段均可以直接在數據庫中看到對應的值,以下圖所示:
由於查看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官方給出的Plugin SDK開發文檔進行了簡短的翻譯,文檔中指出在0.8.0版本以前,Orthanc是利用內建的RESTful API來模擬是實現WADO服務的,並不是是直接響應瀏覽器發送過來的WADO請求。前文中已經介紹瞭如何具體編譯和安裝官方WadoPlugin.dll,這裏在剖析SQLite的基礎上採用單步調試的方式查看一下早期Orthanc是如何利用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中定義所示:
enum ResourceType { ResourceType_Patient = 1, ResourceType_Study = 2, ResourceType_Series = 3, ResourceType_Instance = 4 };
下面直接貼出調試的截圖:
從截圖中能夠看出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文件給出定義,以下:
enum MetadataType { MetadataType_Instance_IndexInSeries = 1, MetadataType_Instance_ReceptionDate = 2, MetadataType_Instance_RemoteAet = 3, MetadataType_Series_ExpectedNumberOfInstances = 4, MetadataType_ModifiedFrom = 5, MetadataType_AnonymizedFrom = 6, MetadataType_LastUpdate = 7, // Make sure that the value "65535" can be stored into this enumeration MetadataType_StartUser = 1024, MetadataType_EndUser = 65535 };
能夠發現其中有RemoteAet類型,所以猜想可能跟DICOM 協議有關,用於記錄上傳端的AE Title,經過輸入指令驗證以下:
指令:storescu.exe -d localhost 4242 -aet ZSSURE -aec ORTHANC c:\Slice_0010.dcm
測試結果:
在分析了原有的效率較低的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標準,數據庫的邏輯設計是很精妙的,後續可深刻研究一下。
fo-dicom搭建簡單的DICOM Server服務器
時間:2014-12-10
預告了很久的幾篇專欄博文一直沒有整理好,主要緣由是早前但願搭建的WML服務器計劃遇到了問題。起初覺得參照DCMTK的官方文檔wwwapp.txt結合前兩天搭建的WAMP服務器能夠順利的實現WML服務,藉此就能夠同時完成WEB PACS系列以及搭建Dicom WML服務器的兩篇博文。但是在實際部署過程當中發現了幾個嚴重的問題,一時沒法解決。可是在搜索解決方案的時候,偶然間找到了在DCMTK論壇上貼出來的用PHP對DCMTK工具包封裝的文章。所以此篇博文在記錄搭建WML遇到的問題的同時,主要想向你們介紹一下這個簡單的封裝DCMTK工具包的PHP類,並在前期搭建的WAMP服務器上給出示範實例。(PS:也但願知道如何解決該問題的大神趕忙現身)
按照DCMTK官方文檔wwwapp.txt文件(http://support.dcmtk.org/docs/file_wwwapp.html)的說明,搭建DICOM Basic Worklist Management服務的前期準備工做已經基本完成,前述的WEB PACS平臺已經可以順利提供HTTPD、CGI以及Perl解析的功能(具體可參見博文中給出的Perl示例:http://blog.csdn.net/zssureqh/article/details/40516745)。可是按照wwwapp.txt文檔指示拷貝wwwapps目錄下的可執行文件(例如preplock、readoviw、readwlst、writwlst)時,並未在編譯後的工程中找到,只看到了相應的.cc源碼文件。
搜索相關資料後,發現柳北風兒前輩此前也遇到過該問題,並向OFFIS的相關維護人員進行過諮詢。前輩的說明博文地址是:http://qimo601.iteye.com/blog/1701026,OFFIS論壇的討論地址是:http://forum.dcmtk.org/viewtopic.php?f=1&t=723&hilit=wwwapp.txt。
按照上述的說明,確信應該是在編譯DCMTK源碼中間的某個環節出現了問題,致使本應該順利生成的幾個exe文件丟失。官方論壇中的討論是針對Linux環境下利用make工具來編譯的狀況,該環境下在利用make安裝的時候由make distclean指令來控制preplock、readoviw、readwlst、writwlst等可執行文件的清除。可是在Windows環境下用的是CMake來生成與VS對應的sln文件,打開DCMTK.sln解決方案後並未找到如何設置才能編譯生成上述可執行文件,並且按照柳前輩的說法,即便編譯成功,在Win7環境下一樣缺乏一個preplock.exe文件。至此該問題的解決就終止了,到發文時刻還未找到很好的解決方法。
在瀏覽OFFIS論壇,尋找上述問題的解決方案時,無心點開了論壇中的「Third-Party DCMTK Applications」分支,以下圖所示,該分論壇中介紹了衆多DCMTK相關的應用開發,其中有一項叫作「PHP DICOM Class「。
其中做者Vedicveko給出了PHP Dicom Class類的設計初衷以及詳細的使用說明,說明文檔網址爲:http://deanvaughan.org/wordpress/dicom-php-class/。
下載源碼(https://github.com/vedicveko/class_dicom.php/zipball/master)後,打開class_dicom.php核心類文件,能夠看出做者經過使用PHP中的exec命令來對DCMTK對應的工具包進行了封裝,藉助於PHP語言的優點使得DCMTK更易於網絡化應用。Apache網絡服務器與PHP之間的調用能夠直接利用咱們前面搭建的簡易WEB PACS平臺(該平臺對於PHP的調用經過FastCGI來實現),而後經過結合PHP DICOM Class能夠實現對dcm文件的大多數操做,具體的實現以下。
第一,將DCMTK編譯後的工具包統一放到指定位置,例如個人本機地址爲:c:\dcmtk\bin,修改class_dicom.php文件中的以下代碼,將TOOLKIT_DIR指向本機工具包目錄c:\dcmtk\bin。
define('TOOLKIT_DIR', 'C:/dcmtk/bin'); // CHANGE THIS IF YOU HAVE DCMTK INSTALLED SOMEWHERE ELSE
第二,藉助前面搭建的WAMP服務,在網站服務根目錄(我本機爲c:\wamp\www\)下新建class_dicom_php目錄,將下載的PHP DICOM Class源碼文件直接拷貝到class_dicom_php目錄下,以下圖所示:
第三,開啓wamp server服務,在瀏覽器中輸入http://localhost/class_dicom_php/examples/get_tags.php,進行測試,正常的話會輸出dean.dcm文件的Tags標籤信息,以下圖所示:
如上所示瀏覽器中看到的結果與利用dcmdump.exe工具查看的結果一致,說明PHP DICOM Class已經順利的安裝到了WEB PACS平臺中。
【注】:在實際運行過程當中可能會出現錯誤,緣由是get_tags.php中使用的是$argv命令行變量來得到具體的dcm文件路徑的,可是在WEB PACS中咱們只能經過GET或者POST方式傳遞參數到php腳本,所以可修改get_tags.php中的參數獲取方式,或者直接將測試文件dean.dcm寫入到文件路徑變量中,以下所示:
$file = (isset($argv[1]) ? $argv[1] : 'dean.dcm');
#原來代碼爲:$file = (isset($argv[1]) ? $argv[1] : ' ');
修改後再次在瀏覽器中輸入http://localhost/class_dicom_php/examples/get_tags.php,就會順利獲得上述結果。
雖然PHP DICOM Class只是簡單的調用了DCMTK工具包來實現PHP對DICOM文件的操做,可是因爲DCMTK工具包的強大,在目前咱們簡易的WEB PACS平臺的併發數不大的狀況下,能夠嘗試直接利用PHP DICOM Class來實現前篇博文中將DCM文件的圖像信息輸出到瀏覽器的功能。
查看class_dicom.php,看到其中dicom_convert類中有關於JPEG到DCM的自由雙向變換,其源碼中用到的是DCMTK工具包中的dcmj2pnm,查看dcmj2pnm的幫助文檔可知,該工具也可實現DCM到bmp文件的轉換,所以決定對class_dicom.php中的dicom_convert類進行擴展,添加dcm_to_bmp()函數,具體代碼以下:
### zssure 20141104 function dcm_to_bmp() { $filesize = 0; $this->jpg_file = $this->file . '.bmp'; $convert_cmd = BIN_DCMJ2PNM . " +ob " . "\"" . $this->file . "\" \"" . $this->jpg_file . "\""; $out = Execute($convert_cmd); if(file_exists($this->jpg_file)) { $filesize = filesize($this->jpg_file); } return($this->jpg_file); }
固然也能夠經過識別dcm具體的圖像標籤來自動設定保存的bmp格式,在自適應時刻用到的主要標籤以下圖所示:
編寫dcm_to_bmp的測試php,代碼以下:
#!/usr/bin/php <?PHP # # Creates a jpeg and jpeg thumbnail of a DICOM file # require_once('../class_dicom.php'); $file = (isset($argv[1]) ? $argv[1] : 'dean.dcm'); if(!$file) { print "USAGE: ./dcm_to_jpg.php <FILE>\n"; exit; } if(!file_exists($file)) { print "$file: does not exist\n"; exit; } $job_start = time(); $d = new dicom_convert; $d->file = $file; $d->dcm_to_bmp(); #$d->dcm_to_tn(); #system("ls -lsh $file*"); $job_end = time(); $job_time = $job_end - $job_start; #print "Created BMP and thumbnail in $job_time seconds.\n"; header("Content-type:image/bmp\n\n"); $jpgName=$d->jpg_file; $fp=fopen($jpgName,"r"); fpassthru($fp); exit; ?>
至此,利用PHP DICOM Class快速便捷地實現了將dcm文件的圖像信息輸出到瀏覽器的功能。
【注】:在上述dcm_to_bmp.php測試文件中須要將#system("ls -lsh $file*");語句註釋掉,不然在windows的WAMP環境下會出現問題。
原來只是利用OFFIS的論壇(http://forum.dcmtk.org/index.php)來搜索使用DCMTK過程當中遇到的各類錯誤,歷來沒有仔細全面的瀏覽過OFFIS論壇的各個部分,經過今天的親身經歷,發如今OFFIS論壇的DCMTK項目下的【Third-Party DCMTK Applications】部分也是一個知識寶藏,裏面包含了各類牛人利用DCMTK開發的工具,大多都是開源的,相關文檔也很詳細,之後能夠做爲重點學習的資料。
下面給出幾個我以爲很值得學習的連接,供你們參考:
利用PHP Skel結合DCMTK開發WEB PACS應用
利用DCMTK搭建WML服務器
利用oracle直接操做DICOM數據
C#的異步編程模式在fo-dicom中的應用
VMWare三種網絡鏈接模式的實際測試
時間:2014-11-04
上一篇博文簡單翻譯了Orthanc官網給出的CodeProject上「利用Orthanc Plugin SDK開發WADO插件」的博文,其中提到了Orthanc從0.8.0版本以後支持快速查詢,而本來的WADO請求須要是直接藉助於Orthanc內部的REST API逐級定位。那麼爲何以前的Orthanc必需要逐級來定位WADO請求的Instance呢?新版本中又是如何進行改進的呢?此篇博文經過分析Orthanc內嵌的SQLite數據庫,來剖析Orthanc的RESTful API機制,以及WADO服務的實現。
上一篇博文中提到的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協議規定的請求鏈接爲:
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,實現最終定位圖像的目的。
上面的實現是否是很繁瑣啊,哈哈。好在官方Plugin SDK說明博文中給出了最新版的定位方式,具體的實現可參見我上一篇博文(http://blog.csdn.net/zssureqh/article/details/41836885)。那麼爲什麼Orthanc起初須要如此繁瑣的定位圖像呢?這裏咱們先簡單的分析一下Orthanc內部是如何來標記文件的惟一性的,後續章節再詳細分析以前Orthanc模擬WADO服務爲什麼如此繁瑣。
在Orthanc源碼中有這樣一個類DicomInstanceHasher(定義在DicomInstanceHasher.h,實如今DicomInstanceHasher.cpp),其註釋中如此描述:
/** * This class implements the hashing mechanism that is used to * convert DICOM unique identifiers to Orthanc identifiers. Any * Orthanc identifier for a DICOM resource corresponds to the SHA-1 * hash of the DICOM identifiers. * \note SHA-1 hash is used because it is less sensitive to * collision attacks than MD5. <a * href="http://en.wikipedia.org/wiki/SHA-256#Comparison_of_SHA_functions">[Reference]</a> **/
從描述中咱們能夠知道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嵌入式數據庫,對數據庫的操做在工程代碼中集成,所以在使用過程當中並未能感受到數據庫的管理,這也支撐了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暫時不清楚,請看下文分析)。
Orthanc源碼中有DatabaseWrapper類,其中有以下注釋:
/** * This class manages an instance of the Orthanc SQLite database. It * translates low-level requests into SQL statements. Mutual * exclusion MUST be implemented at a higher level. **/
說明該類是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數據庫的基本結構後,進行一下實例測試。如博文(http://blog.csdn.net/zssureqh/article/details/41836885)所述,向Orthanc中添加數據有多種方式,命令行工具,REST API,以及網頁。下面咱們對Orthanc自帶的Explorer和DCMTK工具包storescu.exe進行真實數據上傳測試。
先打開Orthanc的瀏覽界面:http://localhost:8042/app/explorer.html#upload
拖拽任意圖像到瀏覽器內,單擊【Start the upload】,直到出現綠色'【Done】,代表上傳成功。
數據庫變化以下:
上述利用Orthanc內嵌的Explorer成功上傳並寫入數據庫。這次使用storescu.exe,把Orthanc當作Dicom Server查看數據寫入狀況,寫入指令以下:
storescu.exe -d localhost 4242 -aet ZSSURE -aec ORTHANC c:\test2.dcm
完成後數據庫變化以下:
上面利用兩種方式來完成了添加數據到Orthanc內嵌SQLite數據庫(還有REST API第三種方式,參見以前博文:,因爲原理與Explorer中類同就不單獨介紹了),而且觀察到了數據庫的真實變化,可是具體的字段含義此刻可能還不是很清楚,讓咱們利用REST API來讀取數據庫並嘗試分析下其中的含義。
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
curl http://localhost:8042/studies
返回結果爲,
即上述分析的Resources表中的每組的第三條記錄,也就是表中的43和47行。
curl http://localhost:8042/series
返回結果爲,
Resources表中每組記錄的第二條,表中的42和46行。
curl http://localhost:8042/instances
返回結果爲,
Resources表中每組記錄的第一條,表中的41和45行。
curl http://localhost:8042/patients/64d6f8a0-ea0ffdb2-a14d1488-4fa7879c-2d9758d8
對比前面數據庫的分析,發現大多數字段均可以直接在數據庫中看到對應的值,以下圖所示:
由於查看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官方給出的Plugin SDK開發文檔進行了簡短的翻譯,文檔中指出在0.8.0版本以前,Orthanc是利用內建的RESTful API來模擬是實現WADO服務的,並不是是直接響應瀏覽器發送過來的WADO請求。前文中已經介紹瞭如何具體編譯和安裝官方WadoPlugin.dll,這裏在剖析SQLite的基礎上採用單步調試的方式查看一下早期Orthanc是如何利用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中定義所示:
enum ResourceType { ResourceType_Patient = 1, ResourceType_Study = 2, ResourceType_Series = 3, ResourceType_Instance = 4 };
下面直接貼出調試的截圖:
從截圖中能夠看出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文件給出定義,以下:
enum MetadataType { MetadataType_Instance_IndexInSeries = 1, MetadataType_Instance_ReceptionDate = 2, MetadataType_Instance_RemoteAet = 3, MetadataType_Series_ExpectedNumberOfInstances = 4, MetadataType_ModifiedFrom = 5, MetadataType_AnonymizedFrom = 6, MetadataType_LastUpdate = 7, // Make sure that the value "65535" can be stored into this enumeration MetadataType_StartUser = 1024, MetadataType_EndUser = 65535 };
能夠發現其中有RemoteAet類型,所以猜想可能跟DICOM 協議有關,用於記錄上傳端的AE Title,經過輸入指令驗證以下:
指令:storescu.exe -d localhost 4242 -aet ZSSURE -aec ORTHANC c:\Slice_0010.dcm
測試結果:
在分析了原有的效率較低的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標準,數據庫的邏輯設計是很精妙的,後續可深刻研究一下。
fo-dicom搭建簡單的DICOM Server服務器