公司原来使用兄弟公司的C++开发的API访问PACS系统,但因为效率等因素,还是需要有我们自己的一套系统,所以我这里使用第三方DICOM DIMSE services插件dicom-dimse-native开发了自己的一套API来访问PACS。
在此之前,我们需要先有一套测试的PACS系统,我们可以用德国offis公司提供的开源项目DCMTK来搭建一个测试PACS服务器。
如何搭建大家可以百度搜索,这里只进行简单流程介绍。
1、win系统的话,从官网下载对应版本安装包。
2、直接解压缩
3、将解压缩后的\etc\dcmtk\目录下dcmqrscp.cfg文件复制一份到\bin目录下,并修改为以下配置:
NetworkTCPPort = 108 #PACS服务器的端口号,下载上传需要用到(注意防火墙允许) MaxPDUSize = 16384 MaxAssociations = 16 HostTable BEGIN #客户机的 AETitle,主机名(客户机命令行打hostname即可看到),端口号(注意防火墙允许) acme1 = (HOSTNAME1, DESKTOP-701Q97F, 5678) acme2 = (HOSTNAME2, jin, 5678) acmeCTcompany = acme1, acme2 HostTable END VendorTable BEGIN "Acme CT Company" = acmeCTcompany #分组 VendorTable END AETable BEGIN #服务器AETitle DICOM文件保存地址 读写权限 最大Studies和字节数 分组 WINQR C:\dcmqrscp RW (200, 1024mb) acmeCTcompany AETable END
4、在\bin目录下启动cmd命令行,执行命令
dcmqrscp.exe -d --config dcmqrscp.cfg
即可启动,如看到以下显示即启动成功
D: $dcmtk: dcmqrscp v3.6.7 2022-04-22 $ D:
5、可以另启动一个cmd窗口(必须到bin目录下),用dcmtk系统自带的工具测试下联通性(假设服务器IP为192.168.1.100)
echoscu.exe -d 192.168.1.100 108 -aec WINQR -aet HOSTNAME1
看到返回的命令行末尾有
I: Received Echo Response (Success) I: Releasing Association
提示则代表联通成功,如果有错误提示则根据错误提示解决,这里不详细介绍。(最好一台服务器一台客户机,不要在一台上测试)
6、现在PACS测试服中还没有DICOM,我们用命令行工具上传一些DICOM文件,假设DICOM文件都在本机C:\dicom目录下,在本机的命令行输入
storescu.exe -d 192.168.1.100 C:\dicom -aec WINQR -aet HOSTNAME2 +sd
即可上传成功,如果提示错误根据提示处理,一般都是一些权限设置,防火墙等问题。
7、在\man目录下有各种工具命令的详细说明文档,可以自行查看。
好了,经过以上步骤我们的PACS测试服已经搭建好了,现在需要我们写NodeJs代码来访问PACS了。
1、从PACS查询DICOM数据
以下是findScu的方法:
/**
* findScu PACS查找DICOM
* @param PatientName 患者姓名
* @param PatientID 患者ID(病历号)
* @param StudyDate 检查日期
* @param instrument 仪器种类
* @returns 数据列表
*/
async findScu(
PatientName: string,
PatientID: string,
StudyDate: string,
instrument: string = 'DX'
): Promise {
const options: findScuOptions = {
source: {
//本机的AETitle,IP,端口号
aet: 'HOSTNAME1',
ip: '192.168.1.99',
port: 5678,
},
target: {
//服务器的AETitle,IP,端口号
aet: 'WINQR',
ip: '192.168.1.100',
port: 108,
},
tags: [
{
key: '00100010', //PatientName 这些是查询条件,不是所有PACS都支持以下所有TAG的查询的,但前三个一般都支持
value: PatientName,
},
{
key: '00100020', //PatientID
value: PatientID,
},
{
key: '00080020', //StudyDate
value: StudyDate,
},
{
key: '00100040', //患者性别
value: '',
},
{
key: '00200010', //StudyID
value: '',
},
{
key: '00100030', //患者出生日期
value: '',
},
{
key: '0020000D', //StudyInstanceUID
value: '',
},
{
key: '00180015', //检查部位
value: '',
},
{
key: '00081090', //设备
value: '',
},
{
key: '00080060', //仪器
value: instrument,
},
{
key: '00080052',
value: 'STUDY',
},
],
verbose: true,
};
return new Promise((resolve, reject) => {
findScu(options, result => {
let rst = JSON.parse(result);
console.log('rst=============================', rst); //打印看看结果
let data = [];
if (rst['code'] == 0 && rst['container']) {
data = JSON.parse(rst['container']);
}
resolve(data);
});
});
}
2、从PACS下载DICOM
/**
* moveScu PACS拉取DICOM
* @param StudyInstanceUID 用唯一的StudyInstanceUID作为下载查询条件
* @returns 返回下载文件存放目录
*/
async moveScu(StudyInstanceUID: string): Promise {
let savePath = 'c:/dicom_save'; //下载的DICOM文件保存地址
const scpOptions: storeScpOptions = {
source: {
//本机的AETitle,IP,端口号
aet: 'HOSTNAME1',
ip: '192.168.1.99',
port: 5678,
},
peers: [
{
//服务器的AETitle,IP,端口号
aet: 'WINQR',
ip: '192.168.1.100',
port: 108,
},
],
storagePath: savePath,
netTransferPrefer: '1.2.840.10008.1.2.4.80', // preferred network transfer syntax (accepted ts - additional to default ts)
netTransferPropose: '1.2.840.10008.1.2.4.80', // proposed network transfer syntax (for outgoing associations - additional to default ts)
writeTransfer: '', // write transfer syntax (storage only, recompress on the fly), keep empty to store ts as original
permissive: false,
verbose: true,
};
//开启传输
startStoreScp(scpOptions, result => {
console.log(
'startStoreScp_result=============================',
JSON.parse(result)
);
});
const moveOptions: moveScuOptions = {
source: {
//本机的AETitle,IP,端口号
aet: 'HOSTNAME1',
ip: '192.168.1.99',
port: 5678,
},
target: {
//服务器的AETitle,IP,端口号
aet: 'WINQR',
ip: '192.168.1.100',
port: 108,
},
tags: [
{
key: '0020000D',
value: StudyInstanceUID,
},
{
key: '00080052',
value: 'STUDY',
},
],
destination: 'HOSTNAME1', // e.g. sending to ourself
verbose: true,
};
return new Promise((resolve, reject) => {
//需要一定延时
setTimeout(() => {
moveScu(moveOptions, result => {
let rst = JSON.parse(result);
if (rst['code'] == '0') {
let dir = path.join(savePath, StudyInstanceUID);
if (fs.existsSync(dir)) {
resolve(dir);
}
}
resolve('');
});
}, 3000);
});
}
3、其他如storescp,storescu等操作,大家可以参考github上的示例