基于cornerstone3D的dicom影像浏览器 第三十一章 从PACS服务加载图像

发布于:2025-06-10 ⋅ 阅读:(22) ⋅ 点赞:(0)


前言

"基于cornerstone3D的dicom影像浏览器"系列文章中都是加载本地文件夹的的dicom图像。
作为一个合格的dicom影像浏览器需要对接PACS服务端,从PACS服务查询检查,下载图像。
本章实现一个查询界面,对接PACS服务。
效果如下 :
在这里插入图片描述


一、两个服务接口

PACS服务需要提供两个接口

  1. 查询检查接口
  2. 查询图像接口

1. 查询检查接口

接口名称可自定义,本文中的接口名称:“http://localhost:9000/queryStudy”
查询参数可自定义,本文中的接口参数有:

startDate    // 检查开始日期
endDate      // 检查结束日期
studyId      // 检查号
modality     // 设备类型
name         // 姓名

示例:
http://localhost:9000/queryStudy?startDate=2000-06-09&endDate=2025-06-09&studyId=2&modality=CT

返回数据示例:

[
    {
        "No": "1",
        "StudyId": "2",
        "PatName": "程**",
        "Sex": "女",
        "Age": "43Y",
        "ImageNum": "1",
        "MzId": "",
        "Modality": "CT",
        "RadiId": "CT0000000001234567",
        "StudyDate": "2020-08-25 10:03:33",
        "ExamItem": "胸部X线计算机体层(CT)平扫",
        "ExamPart": ""
    },
    {
        "No": "2",
        "StudyId": "4",
        "PatName": "赢**",
        "Sex": "女",
        "Age": "63Y",
        "ImageNum": "1",
        "MzId": "",
        "Modality": "CT",
        "RadiId": "CT0000000002",
        "StudyDate": "2020-08-25 10:17:01",
        "ExamItem": "颅脑X线计算机体层(CT)平扫",
        "ExamPart": ""
    },
    {
        "No": "3",
        "StudyId": "13",
        "PatName": "余*",
        "Sex": "女",
        "Age": "37Y",
        "ImageNum": "1",
        "MzId": "",
        "Modality": "CT",
        "RadiId": "CT0000000003",
        "StudyDate": "2022-11-14 16:41:29",
        "ExamItem": "DR胸椎正位 *(1)",
        "ExamPart": ""
    }
    ...
]

2. 查询图像接口

接口名称可自定义,本文中的接口名称:“http://localhost:9000/queryImage”
参数:

studyId   // 检查号

示例:http://localhost:9000/queryImage?studyId=2

返回数据示例:

[
    "http://localhost:9000/pacs/CT/2020-08-25/2/c8353e44b039eca9b334e78b741f3b35/1.dcm",
    "http://localhost:9000/pacs/CT/2020-08-25/2/c8353e44b039eca9b334e78b741f3b35/2.dcm",
    "http://localhost:9000/pacs/CT/2020-08-25/2/c8353e44b039eca9b334e78b741f3b35/3.dcm",
    "http://localhost:9000/pacs/CT/2020-08-25/2/c8353e44b039eca9b334e78b741f3b35/4.dcm",
    ...
]

二、查询界面组件

onSearch 查询检查,显示到列表
onRowClick 查询一次检查所有图像,并归档

<template>
	<div class="pacs">
		<el-dialog
			:title="查询检查"
			draggable
			v-model="visible"
			:modal="false"
			:close-on-click-modal="false"
			width="840px"
			@keydown.native.stop
		>
			<div class="querycond">
				<el-form :model="form" label-width="100px" @submit.prevent>
					<el-row :gutter="0">
						<el-col :span="12">
							<el-form-item :label="检查号:">
								<el-input v-model="form.idNumber"></el-input>
							</el-form-item>
						</el-col>
						<el-col :span="7">
							<el-form-item :label="姓名:">
								<el-input v-model="form.name"></el-input>
							</el-form-item>
						</el-col>
						<el-col :span="5">
							<el-form-item class="right-align" label-width="30px">
								<el-button type="primary" @click="onSearch">
									<el-icon><Search /></el-icon>
									<span style="vertical-align: middle">
										查询
									</span>
								</el-button>
							</el-form-item>
						</el-col>
					</el-row>
					<el-row :gutter="0">
						<el-col :span="12">
							<el-form-item :label="检查日期:">
								<el-date-picker
									v-model="form.queryDate"
									type="daterange"
									align="right"
									unlink-panels
									:range-separator="至"
									:shortcuts="shortcuts"
								>
								</el-date-picker>
							</el-form-item>
						</el-col>
						<el-col :span="7">
							<el-form-item :label="检查类型:">
								<el-select v-model="form.modality" placeholder="">
									<el-option
										v-for="mod in modalities"
										:key="mod"
										:label="mod"
										:value="mod"
									></el-option>
								</el-select>
							</el-form-item>
						</el-col>
						<el-col :span="5">
							<el-form-item class="right-align" label-width="30px">
								<el-button @click="onClear">
									<el-icon><Delete /></el-icon>
									<span style="vertical-align: middle">
										清空
									</span>
								</el-button>
							</el-form-item>
						</el-col>
					</el-row>
				</el-form>
			</div>
			<el-table :data="studyData" border height="420px" @row-dblclick="onRowClick">
				<el-table-column
					property="StudyId"
					:label="检查号"
					width="100"
					:show-overflow-tooltip="true"
				></el-table-column>
				<el-table-column
					property="PatName"
					:label="姓名"
					width="100"
					:show-overflow-tooltip="true"
				></el-table-column>
				<el-table-column
					property="Sex"
					:label="性别"
					width="80"
				></el-table-column>
				<el-table-column
					property="Age"
					:label="年龄"
					width="80"
				></el-table-column>
				<el-table-column
					property="StudyDate"
					:label="检查日期"
					width="168"
					:show-overflow-tooltip="true"
				></el-table-column>
				<el-table-column
					property="Modality"
					:label="设备"
					width="90"
				></el-table-column>
				<el-table-column
					property="ExamItem"
					:label="检查项目"
					width="220"
					:show-overflow-tooltip="true"
				></el-table-column>
			</el-table>
		</el-dialog>
	</div>
</template>

<script lang="js" setup name="SearchPACS">
import { ref, reactive, onMounted } from "vue";
import { desensitizeSubstring } from "@/utils";
import { useArchiveStore } from "../stores/archive";
import { ElMessage } from "element-plus";

const archiveStore = useArchiveStore();
const studyData = ref([]);
const visible = ref(false);
const modalities = ["ALL", "CR", "DX", "MG", "CT", "MR", "RF", "OT", "XA"];
const form = reactive({
	idNumber: "",
	name: "",
	modality: "ALL",
	queryDate: [new Date(), new Date()],
});

const shortcuts: [
	{
		text: "今天",
		value: () => {
			const end = new Date();
			const start = new Date();
			return [start, end];
		},
	},
	{
		text: "昨天",
		value: () => {
			const end = new Date();
			const start = new Date();
			start.setTime(start.getTime() - 3600 * 1000 * 24);
			return [start, end];
		},
	},
	{
		text: "最近三天",
		value: () => {
			const end = new Date();
			const start = new Date();
			start.setTime(start.getTime() - 3600 * 1000 * 24 * 3);
			return [start, end];
		},
	},
	{
		text: "最近一周",
		value: () => {
			const end = new Date();
			const start = new Date();
			start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
			return [start, end];
		},
	},
	{
		text: "最近一个月",
		value: () => {
			const end = new Date();
			const start = new Date();
			start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
			return [start, end];
		},
	},
	{
		text: "最近一年",
		value: () => {
			const end = new Date();
			const start = new Date();
			start.setTime(start.getTime() - 3600 * 1000 * 24 * 365);
			return [start, end];
		},
	},
];

const show = () => {
	visible.value = true;
};

const onSearch = () => {
	// 查询检查服务接口,实际可存储到配置中
	const studyapi = "http://localhost:9000/queryStudy";
	let url = studyapi;

	const sdate = form.queryDate[0];
	const edate = form.queryDate[1];

	url += "?startDate=" + sdate + "&endDate=" + edate;

	if (form.modality !== "ALL") {
		url += "&modality=" + form.modality;
	}
	if (form.idNumber) {
		url += "&id=" + form.idNumber;
	}
	if (form.name) {
		url += "&name=" + form.name;
	}

	//console.log(url);

	fetch(url)
		.then((response) => response.json())
		.then((data) => {
			// console.log(data);
			studyData.value = data.map((item) => {
				return {...item, PatName: desensitizeSubstring(item.PatName, 1, -1) }
			});
			// console.log(studyData.value);
		})
		.catch((error) => {
			console.log(error);
			ElMessage.error("查询失败");
		});

};

const onClear = () => {
	form.idNumber = "";
	form.name = "";
	form.modality = "ALL";
	form.queryDate = [new Date(), new Date()];
	studyData.value = [];
};

const onRowClick = (row) => {
	// 查询图像服务接口,实际可存储到配置中
	const imageapi = "http://localhost:9000/queryImage";

	let url;
	let params = "studyId=" + row.StudyId;
	url = imageapi + "?" + params;

	// console.log(url);
	ElMessage.success("正在获取图像, 请稍候...");
	visible.value = false;

	fetch(url)
		.then((response) => response.json())
		.then((data) => {
			// console.log(data);
			const imageIds = data.map((item) => "dicomweb:" + item);
			//console.log("imageIds: ", imageIds);
			imageIds.forEach((imageId) => {
				archiveStore.archiveFile(imageId);
			});
			
		})
		.catch((error) => {
			console.log(error);
			ElMessage.error("查询失败");
		});
};

defineExpose({
	show,
});

</script>

<style lang="scss" scoped>
:deep(.el-dialog) {
	padding: 1px;
	border: 1px solid gray;
	border-radius: 4px;
}
:deep(.el-dialog__header) {
	background-color: #eee;
	height: 40px;
	padding: 8px 8px;
	border: none;
}

:deep(.el-dialog__title) {
	color: #444;
}


:deep(.el-table__body-wrapper) {
	background-color: white;
}

:deep(.el-table__header .is-leaf) {
	background-color: white;
	color: #444;
}

:deep(.el-table__cell) {
	background-color: white;
	color: #444;
	padding: 4px 0px;
}

:deep(.el-table--enable-row-hover .el-table__body tr:hover > td) {
	background-color: #d3e3fd;
}

:deep(.el-table--border .el-table__cell:first-child .cell) {
	padding-left: 4px;
}

.querycond {
	width: 100%;
	height: 100px;
	padding: 8px 0;
	background-color: white;
}

:deep(.el-form-item) {
	margin-bottom: 4px;
}

:deep(.el-form-item__label) {
	color: #444;
}

:deep(.el-form-item__content .el-input .el-input__inner) {
	height: 30px;
	line-height: 30px;
	padding: 0px 8px;
}

:deep(.el-button) {
	height: 32px;
	width: 100px;
	line-height: 32px;
	padding: 0;
}

:deep(.el-range-editor.el-input__inner) {
	height: 32px;
	padding: 0 10px;
}
:deep(.el-date-editor--daterange) {
	width: 320px;
}

:deep(.el-date-editor .el-range-separator) {
	padding: 0;
	width: 8%;
}
</style>

三、修改归档

之前只对本地文件归档,现增加对网络文件归档的兼容
归档逻辑请查看第四章 加载本地文件夹中的dicom文件并归档

修改archiveStore.js文件中的archiveFile函数。
修改前:

async function archiveFile(file) {
		const imageId = cornerstoneDICOMImageLoader.wadouri.fileManager.add(file);
		
		const dcmImage = new DCMImage({ imageId });
		await dcmImage.parse();
		...
}

修改后:

async function archiveFile(file) {
	let imageId = "";
	if (typeof file === "string") {
		imageId = file;
	} else {
		imageId = cornerstoneDICOMImageLoader.wadouri.fileManager.add(file);
	}

	const dcmImage = new DCMImage({ imageId });
	await dcmImage.parse();
	...
}

总结

本章实现从PACS服务器加载dicom图像。
说明了对接PACS服务所需的两个接口的定义和返回数据格式。