记录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)没能结束连接,不知道有没有知道的小伙伴可以告知。