当前位置: 首页 > 工具软件 > dcm4che > 使用案例 >

Java 使用dcm4che的工具类findscu查询pacs数据

翟俊茂
2023-12-01

dcm4chee-tool-findscu的使用

记录Java对接pacs系统并进行查询

准备

我这里使用的是springboot+jdk1.8,并且已经在本地安装了一个dcm4chee-arc来模拟pacs系统。

开始

1、修改pom.xml添加dcm4che依赖

<!-- dcm4che start -->
<dependency>
   <groupId>org.apache.commons</groupId>
   <artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
   <groupId>commons-cli</groupId>
   <artifactId>commons-cli</artifactId>
   <version>1.4</version>
</dependency>
<dependency>
   <groupId>org.dcm4che.tool</groupId>
   <artifactId>dcm4che-tool-common</artifactId>
   <version>5.25.2</version>
</dependency>
<dependency>
   <groupId>org.dcm4che</groupId>
   <artifactId>dcm4che-imageio</artifactId>
   <version>5.25.2</version>
</dependency>
<!--dcm end-->

2、新建FindSCU,参考链接: dcm4che官网长辞笙-初次使用dcm4che-tool-findscu做查询

import com.evo.common.BusinessException;
import com.evo.pojo.PacsData;
import org.apache.commons.cli.ParseException;
import org.apache.commons.lang3.StringUtils;
import org.dcm4che3.data.Attributes;
import org.dcm4che3.data.Tag;
import org.dcm4che3.data.UID;
import org.dcm4che3.data.VR;
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 org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

@Component
public class FindSCU {
    public static enum InformationModel {
        PatientRoot(UID.PatientRootQueryRetrieveInformationModelFind, "STUDY"),
        StudyRoot(UID.StudyRootQueryRetrieveInformationModelFind, "STUDY"),
        PatientStudyOnly(UID.PatientStudyOnlyQueryRetrieveInformationModelFind, "STUDY"),
        MWL(UID.ModalityWorklistInformationModelFind, null),
        UPSPull(UID.UnifiedProcedureStepPull, null),
        UPSWatch(UID.UnifiedProcedureStepWatch, null),
        UPSQuery(UID.UnifiedProcedureStepQuery, null),
        HangingProtocol(UID.HangingProtocolInformationModelFind, null),
        ColorPalette(UID.ColorPaletteQueryRetrieveInformationModelFind, null);

        final String cuid;
        final String level;

        InformationModel(String cuid, String level) {
            this.cuid = cuid;
            this.level = level;
        }

        public void adjustQueryOptions(EnumSet<QueryOption> queryOptions) {
            if (level == null) {
                queryOptions.add(QueryOption.RELATIONAL);
                queryOptions.add(QueryOption.DATETIME);
            }
        }
    }

    private static ResourceBundle rb = ResourceBundle.getBundle("static/message", Locale.getDefault());

    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() {
        EnumSet<QueryOption> queryOptions = EnumSet.noneOf(QueryOption.class);
        queryOptions.add(QueryOption.FUZZY);
        return queryOptions;
    }

    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 List<PacsData>  query() throws IOException, InterruptedException {
        return query(keys);
    }

    private List<PacsData> query(Attributes keys) throws IOException, InterruptedException {
        List<PacsData> pacsDataList = new ArrayList<>();
        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);
                if (Status.isPending(status)) {
                    ++numMatches;
                    PacsData pacsData = FindSCU.this.printResult(data);
                    pacsDataList.add(pacsData);
                    if (cancelAfter != 0 && numMatches >= cancelAfter)
                        try {
                            cancel(as);
                            cancelAfter = 0;
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                }
            }
        };
        query(keys, rspHandler);
        return pacsDataList;
    }

    private void query(Attributes keys, DimseRSPHandler rspHandler) throws IOException, InterruptedException {
        as.cfind(model.cuid, priority, keys, null, rspHandler);
        //as.cget(model.cuid, priority, keys, null, rspHandler);
    }

    private PacsData 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");
        }
        PacsData pacsData = new PacsData();
        pacsData.setPatientId(data.getString(Tag.PatientID));
        pacsData.setPatientName(data.getString(Tag.PatientName));
        pacsData.setPatientBirthDate(data.getDate(Tag.PatientBirthDate));
        pacsData.setNumberOfStudyRelatedSeries(data.getString(Tag.NumberOfStudyRelatedSeries));
        pacsData.setPatientWeight(data.getString(Tag.PatientWeight));
        pacsData.setPatientAge(data.getString(Tag.PatientAge));
        pacsData.setPatientSex(data.getString(Tag.PatientSex));
        pacsData.setPregnancyStatus(data.getString(Tag.PregnancyStatus));
        pacsData.setInstitutionName(data.getString(Tag.InstitutionName));
        pacsData.setAccessionNumber(data.getString(Tag.AccessionNumber));
        pacsData.setStudyId(data.getString(Tag.StudyID));
        pacsData.setStudyInstanceUid(data.getString(Tag.StudyInstanceUID));
        pacsData.setStudyDate(data.getDate(Tag.StudyDate));
        pacsData.setModality(data.getString(Tag.Modality));
        pacsData.setModalitiesInStudy(data.getString(Tag.ModalitiesInStudy));
        pacsData.setStudyDescription(data.getString(Tag.StudyDescription));
        //pacsData.setBodyPartExamined(data.getString(Tag.BodyPartExamined));
        pacsData.setProtocolName(data.getString(Tag.ProtocolName));
        pacsData.setSeriesDescription(data.getString(Tag.SeriesDescription));
        pacsData.setSeriesInstanceUID(data.getString(Tag.SeriesInstanceUID));
        pacsData.setNumberOfSeriesRelatedInstances(data.getString(Tag.NumberOfSeriesRelatedInstances));
        pacsData.setNumberOfStudyRelatedInstances(data.getString(Tag.NumberOfStudyRelatedInstances));
        return pacsData;
    }

    /**
     * 配置远程连接
     *
     * @param conn Connection
     * @param rq   AAssociateRQ
     */
    private void configureConnect(Connection conn, AAssociateRQ rq) throws ParseException {
        // 获取title属性值
        //String title = "MYAET";//修改成你的
        String title = rb.getString("title");
        if (StringUtils.isBlank(title)) {
            throw new ParseException("title cannot be missing");
        }
        // 设置AE title
        rq.setCalledAET(title);
        // 读取host和port属性值
        //String host = "192.168.3.86";//修改成你的
        String host = rb.getString("host");
        //String port = "11112";//修改成你的
        String port = rb.getString("port");
        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 List<PacsData> 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);
                List<PacsData> query = main.query();// 查询
                return query;
            } finally {
                main.close();
                executorService.shutdown();
                scheduledExecutorService.shutdown();

            }
        } catch (ParseException | InterruptedException | IncompatibleConnectionException | GeneralSecurityException
                | IOException e) {
            e.printStackTrace();
            throw new BusinessException(e.getMessage());
        }
    }

}



message.properties

title=要连接的title
host=要连接的的ip
port=要连接的端口号
#specifies Information Model. Supported names: PatientRoot, StudyRoot, PatientStudyOnly,
#MWL, UPSPull, UPSWatch, UPSQuery, HangingProtocol or ColorPalette. If no Information Model is specified,
#StudyRoot will be used.
model=
invalid-model-name={0} is not a supported Information Model name
#specifies retrieve level. Use STUDY for PatientRoot, StudyRoot, PatientStudyOnly by default.
level=SERIES
#cancel the query request after the receive of the specified number of matches.
cancel=1
xml=write received matches as XML Infoset specified in DICOM Part 19
xsl=apply specified XSLT stylesheet to XML representation of received matches; implies -X
prior-high=
prior-low=

修改了官网的命令行式的使用方式而改为java常用方式,我这里的level声明为SERIES,可以查找到STUDY下一级的数据。
官网的查询没有返回值,我这边的因为要拿到所有数据,所以改变了返回值,如果你们只需要保存数据库或者可以直接操作可以不用这样做。

使用

 //设置dcm参数
Attributes attrs = new Attributes();
 // 查询条件 相当于命令 findscu -c DCM4CHEE@192.168.100.55:11112 -m ModalitiesInStudy=MR
 attrs.setString(Tag.ModalitiesInStudy, VR.CS, "PT", "CT");
 //attrs.setString(Tag.Modality, VR.CS);
 attrs.setString(Tag.NumberOfStudyRelatedSeries, VR.IS);
 attrs.setString(Tag.PatientID, VR.LO);
 ...
 //想获取的参数要先声明attrs.set
 //调用findscu
  List<PacsData> pacsDataList = findSCU.matchingKeys(attrs);

这种拿到的数据可以根据自己的需求进行再封装。
3、结尾
这就是全部关于findscu的使用啦,但是我没有找到分页的位置在哪,我觉得在

if (cancelAfter != 0 && numMatches >= cancelAfter)
       try {
           cancel(as);
           cancelAfter = 0;
       } catch (IOException e) {
           e.printStackTrace();
       }
 ```当查询到的数目大于你设置的cancelAfter就不在查询,但是设置完之后好像没生效,cancel(as)没能结束连接,不知道有没有知道的小伙伴可以告知。
 类似资料: