全网实现workList服务的,要么是基于C++的DCMTK、要么是基于C#的fo-dicom。想用dcm4che实现 找了好几个月都没有一个例子。无奈只能通过DCMTK和fo-dicom 实现方式并查看dcm4che源码自己实现了。经过不懈的努力总算是实现了并实际跟设备测试成功!
首先得先了解 DICOM worklist工作原理?
一、关于Worklist
在RIS与PACS的系统集成中。Wordlist的连接bai为其主要工作之一。Wordlist成像设备工作列表,它是DICOM协议中众多服务类别中的一个.它的功能是实现设备操作台与登记台之间的通讯,完成成像设备和信息系统的集成.称为BASIC WORKLIST MANAGEMENT SERVICE(简称Worklist)。
二、DICOM标准中与Worklist相关的一些基本概念
配置影像检查设备(Modality)的Worklist首先要阅读该设备的“DICOM 一致性声明(DICOM Conformance Statement)”中关于Worklist的部分,了解设备对Worklist的支持程度。而熟悉以下基本概念则有助于阅读DICOM Conformance Statement:
1、VR(Value Representation):描述了数据元素的种类(字符串、数字、日期等)以及这些值的格式。在DICOM标准第五部分Data Structures and Encoding的第25页中列出了所有的VR。
2、Data Set(数据集):一个数据集表示了一个DICOM对象,它进一步由Data Element(数据元素)组成。而数据元素包括了tag(唯一的)、值的长度以及值。数据元素中可能包含VR。
3、 数据元素类型:一个数据元素是否在一个数据集中出现,取决于该数据元素的类型。
4、 AE Title:AE Title(Application Entity Title)是配置影像检查设备DICOM服务(Worklist、Storage、Print等)必不可少的参数之一。对于某一台影像检查设备,其各个DICOM服务可以对应不同的AE Title,当然这些DICOM服务也可以对应同一个AE Title。AE Title是一个字符串,但是这个字符串在我们要配置的RIS/PACS系统的网络中必须是唯一的。因此,AE Title是这个网络中某一个(或几个)DICOM服务的唯一标识。
三、DICOM的Worklist实现的功能
fz2841585:从RIS或者其他系统下载病人信息,以免重复登记。
0753zhongwei:Worklist只是一个传输协议,DICOM的Worklist其实就是C-FIND服务,有点类似于Query/Retrieve,SCU在C-FIND命令集后面加上一些查询字段,SCP把查询结果放在C-FIND-RSP后面返回去。
tks1000:在CT或MR等工作站上,如果没有Worklist功能,新检查一个病人的时候,要输入病人全部的基本信息,这样比较麻烦,而且容易出错。有了WorkList功能后,可以直接从服务器上读取病人的基本信息,不用输入,而且不易出错。实质上还是C-FIND,不过需要MPPS等的支持。
chaoran898:DICOM的MWL是一种接口协议,至于怎样查数据,那是coding实现的事情,MWL只负责把找到的数据按DICOM标准传出去。
xiaoyilong19:我做了多台设备的Worklist,深有体会:如果设备厂家不同的话,Worklist服务端程序就要调试一番,才能让返回的数据在对方设备工作站上显示出来,否则就是出现各种情况。乱码还比较简单处理,就是怕对方什么应答都没有。实际上就是,请求,返回请求,和cs服务架构一样。
四、Worklist在Pacs中的作用与的工作原理
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiclRnblN2XjlGcjAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHL90EVPhXRE1EeBRVT3V1MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLwgjN2ADNzQTMyETMxAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
五、 基本设计概念和处理流程
xuyuansheng:正常的流程是,病人在HIS上注册,经hl7消息传至RIS,RIS上便有了病人的登记信息。做检查时,成像设备通过DICOM Worklist来从RIS上取得需做检查的病人列表,选择后做检查。检查完成后,图像便可以传到PACS中进行存储。在这个过程中,病人信息仅在HIS端输入一遍,但它流经RIS,Modality以及PACS。可以节省时间,减少错误,规范流程,互联互通,形成数据共享。理想的情况下,让医生专注于检查及诊断,而缩短的时间,也会提高病人的满意度。
六、总结
看到这里后大概知道 workList 其实就是一个客户端(SCU)发起C-Find请求 服务端(SCP)将这些结果按照DICOM协议返回对应的字段组合即可
接下来就该查看DCM4CHE源码了
我们现在知道了workList 其实就是C-Find请求 我们就在dcm4che源码里找关于c-find的一切内容了!
原来dcm4che源码中dcm4che-tool 有个dcm4che-tool-findscu
很是惊喜 总算找到个入口了
甚至还有命令示例,NICE!
例子:
findscu -c [email protected]:11112 -m PatientName=Doe^John -m
StudyDate=20110510- -m ModalitiesInStudy=CT
github上对各个命令都有解释,这里简单解释一下这个例子
-c 代表远程连接
DCMQRSCP是指dcm4che服务的AETitle
localhost是指dcm4che服务的ip地址
11112是指dcm4che服务的端口
-m PatientName=Doe^John 代表查询患者姓名是Doe^John
-m StudyDate=20110510 代表查询患者检查时间是20110510
-m ModalitiesInStudy=CT 代表查询患者的模态是CT
命令也支持xml文件查询和结果导出xml
甚至dcm4chee 文件里有执行findscu 命令脚本!是不是感觉离成功近了一大步~~
先试试脚本命令
(本地局域网已经部署好了 dcm4chee-web 可以直接把它当作worklist scp)
确实可以建立了通讯并且可以查询 那接下来就从 FindSCU.java源码下功夫了
充分阅读源码后 将源码进行改造
直击源码里的main 方法
public static void main(String[] args) {
try {
CommandLine cl = parseComandLine(args);//解析命令
FindSCU main = new FindSCU();
CLIUtils.configureConnect(main.remote, main.rq, cl); 设置连接ip和端口
CLIUtils.configureBind(main.conn, main.ae, cl);
CLIUtils.configure(main.conn, cl);
main.remote.setTlsProtocols(main.conn.getTlsProtocols());// 设置Tls协议
main.remote.setTlsCipherSuites(main.conn.getTlsCipherSuites());
configureServiceClass(main, cl);
configureKeys(main, cl);
configureOutput(main, cl);// 设置检索级别
configureCancel(main, cl);// 配置 --cancel
main.setPriority(CLIUtils.priorityOf(cl));
ExecutorService executorService =
Executors.newSingleThreadExecutor();
ScheduledExecutorService scheduledExecutorService =
Executors.newSingleThreadScheduledExecutor();
main.device.setExecutor(executorService);
main.device.setScheduledExecutor(scheduledExecutorService);
try {
main.open();// 打开链接
List<String> argList = cl.getArgList();
if (argList.isEmpty())
main.query();// 查询 这里是重点
else
for (String arg : argList)
main.query(new File(arg));
} finally {
main.close();
executorService.shutdown();
scheduledExecutorService.shutdown();
}
} catch (ParseException e) {
System.err.println("findscu: " + e.getMessage());
System.err.println(rb.getString("try"));
System.exit(2);
} catch (Exception e) {
System.err.println("findscu: " + e.getMessage());
e.printStackTrace();
System.exit(2);
}
}
具体看query 方法
public void query( DimseRSPHandler rspHandler) throws IOException, InterruptedException {
query(keys, rspHandler);
}
private void query(Attributes keys, DimseRSPHandler rspHandler) throws IOException, InterruptedException {
as.cfind(model.cuid, priority, keys, null, rspHandler);
}
主要看懂源码的这两个地方 大概实现findscu 就有思路了
需要的maven 包
<dependency>
<groupId>org.dcm4che</groupId>
<artifactId>dcm4che-core</artifactId>
<version>5.16.1</version>
</dependency>
<dependency>
<groupId>org.dcm4che</groupId>
<artifactId>dcm4che-net</artifactId>
<version>5.16.1</version>
</dependency>
<dependency>
<groupId>org.dcm4che.tool</groupId>
<artifactId>dcm4che-tool-common</artifactId>
<version>5.16.1</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>org.dcm4che</groupId>
<artifactId>dcm4che-imageio</artifactId>
<version>5.16.1</version>
</dependency>
<!-- lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.dcm4che</groupId>
<artifactId>dcm4che-imageio-opencv</artifactId>
<version>5.16.1</version>
<scope>runtime</scope>
</dependency>
</dependencies>
改造后的findscu .java
import com.javasm.entity.enums.InformationModel;
import org.apache.commons.cli.ParseException;
import org.apache.commons.lang3.StringUtils;
import org.dcm4che3.data.*;
import org.dcm4che3.net.*;
import org.dcm4che3.net.pdu.AAssociateRQ;
import org.dcm4che3.net.pdu.ExtendedNegotiation;
import org.dcm4che3.net.pdu.PresentationContext;
import org.dcm4che3.util.SafeClose;
import java.io.*;
import java.security.GeneralSecurityException;
import java.text.MessageFormat;
import java.util.EnumSet;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
public class FindSCU {
private static String[] IVR_LE_FIRST = new String[]{"1.2.840.10008.1.2", "1.2.840.10008.1.2.1",
"1.2.840.10008.1.2.2"};
private final Device device = new Device("findscu");
private final ApplicationEntity ae = new ApplicationEntity("FINDSCU");
private final Connection conn = new Connection();
private final Connection remote = new Connection();
private final AAssociateRQ rq = new AAssociateRQ();
private int priority;
private int cancelAfter;
private InformationModel model;
private Attributes keys = new Attributes();
private OutputStream out;
private Association as;
private FindSCU() {
device.addConnection(conn);
device.addApplicationEntity(ae);
ae.addConnection(conn);
}
private void setPriority(int priority) {
this.priority = priority;
}
private void setInformationModel(InformationModel model, String[] tss, EnumSet<QueryOption> queryOptions) {
this.model = model;
rq.addPresentationContext(new PresentationContext(1, model.cuid, tss));
if (!queryOptions.isEmpty()) {
model.adjustQueryOptions(queryOptions);
rq.addExtendedNegotiation(
new ExtendedNegotiation(model.cuid, QueryOption.toExtendedNegotiationInformation(queryOptions)));
}
if (model.level != null)
addLevel(model.level);
}
private void addLevel(String s) {
keys.setString(Tag.QueryRetrieveLevel, VR.CS, s);
}
private void setCancelAfter(int cancelAfter) {
this.cancelAfter = cancelAfter;
}
private static EnumSet<QueryOption> queryOptionsOf() {
return EnumSet.noneOf(QueryOption.class);
}
private static void configureCancel(FindSCU main) {
if (StringUtils.isNotBlank(rb.getString("cancel"))) {
main.setCancelAfter(Integer.parseInt(rb.getString("cancel")));
}
}
private static void configureRetrieve(FindSCU main) {
if (StringUtils.isNotBlank(rb.getString("level"))) {
// Retrieve是指SCU通过Query 拿到信息后,要求对方根据请求级别 (Patient/Study/Series/Image) 发送影像给己方。
// 默认Patient
main.addLevel(rb.getString("level"));
}
}
/**
* 设置Information Model
*
* @param main
* @throws ParseException
*/
private static void configureServiceClass(FindSCU main) throws ParseException {
main.setInformationModel(informationModelOf(), IVR_LE_FIRST, queryOptionsOf());
}
private static InformationModel informationModelOf() throws ParseException {
try {
String model = rb.getString("model");
// 如果model为空,默认StudyRoot
return StringUtils.isNotBlank(model) ? InformationModel.valueOf(model) : InformationModel.StudyRoot;
} catch (IllegalArgumentException e) {
throw new ParseException(MessageFormat.format(rb.getString("invalid-model-name"), rb.getString("model")));
}
}
private static int priorityOf() {
String high = rb.getString("prior-high");
String low = rb.getString("prior-low");
return StringUtils.isNotBlank(high) ? 1 : (StringUtils.isNotBlank(low) ? 2 : 0);
}
private void open()
throws IOException, InterruptedException, IncompatibleConnectionException, GeneralSecurityException {
as = ae.connect(conn, remote, rq);
}
private void close() throws IOException, InterruptedException {
if (as != null && as.isReadyForDataTransfer()) {
as.waitForOutstandingRSP();
as.release();
}
SafeClose.close(out);
out = null;
}
private void configureKeys(Attributes keys) {
this.keys.addAll(keys);
}
private void query() throws IOException, InterruptedException {
query(keys);
}
private void query(Attributes keys) throws IOException, InterruptedException {
DimseRSPHandler rspHandler = new DimseRSPHandler(as.nextMessageID()) {
int cancelAfter = FindSCU.this.cancelAfter;
int numMatches;
@Override
public void onDimseRSP(Association as, Attributes cmd, Attributes data) {
super.onDimseRSP(as, cmd, data);
int status = cmd.getInt(Tag.Status, -1);
FindSCU.this.printResult(data);
if (Status.isPending(status)) {
++numMatches;
if (cancelAfter != 0 && numMatches >= cancelAfter)
try {
cancel(as);
cancelAfter = 0;
} catch (IOException e) {
e.printStackTrace();
}
}
}
};
query(keys, rspHandler);
}
private void query(Attributes keys, DimseRSPHandler rspHandler) throws IOException, InterruptedException {
as.cfind(model.cuid, priority, keys, null, rspHandler);
}
private void printResult(Attributes data) {
String SpecificCharacterSet = data.getString(Tag.SpecificCharacterSet);
// 设置编码,防止乱码
if (StringUtils.isBlank(SpecificCharacterSet)) {
data.setString(Tag.SpecificCharacterSet, VR.CS, "GB18030");
data.setString(Tag.SpecificCharacterSet, VR.PN, "GB18030");
}
// 打印查询结果
System.out.println("---------- patient -----------");
System.out.println("PatientID : " + data.getString(Tag.PatientID)); // 患者唯一ID
System.out.println("PatientName : " + data.getString(Tag.PatientName)); // 患者姓名
System.out.println("PatientBirthDate : " + data.getDate(Tag.PatientBirthDate)); // 出生日期
System.out.println("PatientSex : " + data.getString(Tag.PatientSex)); // 患者性别
System.out.println("PatientWeight : " + data.getString(Tag.PatientWeight)); // 患者体重
System.out.println("PregnancyStatus : " + data.getString(Tag.PregnancyStatus)); // 怀孕状态
System.out.println("InstitutionName : " + data.getString(Tag.InstitutionName)); // 医院名称
System.out.println();
System.out.println("----------- study ------------");
System.out.println("AccessionNumber : " + data.getString(Tag.AccessionNumber)); // 检查号:RIS的生成序号,用于标识做检查的次序
System.out.println("StudyID : " + data.getString(Tag.StudyID)); // 检查ID
System.out.println("StudyInstanceUID : " + data.getString(Tag.StudyInstanceUID)); // Study Instance UID 检查实例号,用于标识检查的唯一ID
System.out.println("StudyDate : " + data.getDate(Tag.StudyDate)); // 检查日期时间
System.out.println("Modality : " + data.getString(Tag.Modality)); // 检查类型
System.out.println("ModalitiesInStudy : " + data.getString(Tag.ModalitiesInStudy)); // 检查类型
System.out.println("PatientAge : " + data.getString(Tag.PatientAge)); // 做检查时刻的患者年龄
System.out.println("StudyDescription : " + data.getString(Tag.StudyDescription)); // 检查描述信息
System.out.println("BodyPartExamined : " + data.getString(Tag.BodyPartExamined)); // 检查部位
System.out.println("ProtocolName : " + data.getString(Tag.ProtocolName)); // 协议名称
System.out.println();
}
/**
* 配置远程连接
*
* @param conn Connection
* @param rq AAssociateRQ
*/
private static void configureConnect(Connection conn, AAssociateRQ rq) throws ParseException {
// 获取title属性值
String title = "AEtitle";//修改成你的
if (StringUtils.isBlank(title)) {
throw new ParseException("title cannot be missing");
}
// 设置AE title
rq.setCalledAET(title);
// 读取host和port属性值
String host = "127.0.0.1";//修改成你的
String port = "8080";//修改成你的
if (StringUtils.isBlank(host) || StringUtils.isBlank(port)) {
throw new ParseException("host or port cannot be missing");
}
// 设置host和por
conn.setHostname(host);
conn.setPort(Integer.parseInt(port));
}
public static void matchingKeys(Attributes attrs) {
try {
FindSCU main = new FindSCU();
configureConnect(main.remote, main.rq); // 设置连接ip和端口 (远程)
main.remote.setTlsProtocols(main.conn.getTlsProtocols()); // 设置Tls协议
main.remote.setTlsCipherSuites(main.conn.getTlsCipherSuites());
configureServiceClass(main); // 设置Information Model
configureRetrieve(main); // 设置检索级别
configureCancel(main); // 配置 --cancel
main.setPriority(priorityOf()); // 设置优先级
ExecutorService executorService = Executors.newSingleThreadExecutor(); // 单线程化线程池
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); // 定时任务
main.device.setExecutor(executorService);
main.device.setScheduledExecutor(scheduledExecutorService);
try {
main.open(); // 打开链接
main.configureKeys(attrs);
main.query(); // 查询
} finally {
main.close();
executorService.shutdown();
scheduledExecutorService.shutdown();
}
} catch (ParseException | InterruptedException | IncompatibleConnectionException | GeneralSecurityException
| IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Attributes attrs = new Attributes();
attrs.setString(Tag.ModalitiesInStudy, VR.CS, "MR");
// 查询展示的信息
attrs.setString(Tag.PatientID, VR.LO);
attrs.setString(Tag.PatientName, VR.PN);
attrs.setString(Tag.PatientBirthDate, VR.DA);
attrs.setString(Tag.PatientSex, VR.CS);
attrs.setString(Tag.PatientWeight, VR.DS);
attrs.setString(Tag.PregnancyStatus, VR.US);
attrs.setString(Tag.InstitutionName, VR.LO);
attrs.setString(Tag.AccessionNumber, VR.SH);
attrs.setString(Tag.StudyID, VR.SH);
attrs.setString(Tag.StudyInstanceUID, VR.UI);
attrs.setString(Tag.StudyDate, VR.DA);
attrs.setString(Tag.Modality, VR.CS);
attrs.setString(Tag.PatientAge, VR.AS);
attrs.setString(Tag.StudyDescription, VR.LO);
attrs.setString(Tag.BodyPartExamined, VR.CS);
attrs.setString(Tag.ProtocolName, VR.LO);
FindSCU.matchingKeys(attrs);
}
}
再上个成功查询的图