天天看点

java dcm4che findscu实现workList通讯——客户端SCU

全网实现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中的作用与的工作原理

java dcm4che findscu实现workList通讯——客户端SCU

五、 基本设计概念和处理流程

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

java dcm4che findscu实现workList通讯——客户端SCU
java dcm4che findscu实现workList通讯——客户端SCU

很是惊喜 总算找到个入口了

java dcm4che findscu实现workList通讯——客户端SCU

甚至还有命令示例,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

java dcm4che findscu实现workList通讯——客户端SCU

甚至dcm4chee 文件里有执行findscu 命令脚本!是不是感觉离成功近了一大步~~

先试试脚本命令

(本地局域网已经部署好了 dcm4chee-web 可以直接把它当作worklist scp)

java dcm4che findscu实现workList通讯——客户端SCU

确实可以建立了通讯并且可以查询 那接下来就从 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);
    }
}

           

再上个成功查询的图

java dcm4che findscu实现workList通讯——客户端SCU