feat: 上传全部影像

This commit is contained in:
mozzie 2023-09-01 14:11:16 +08:00
parent 976d687b1b
commit 184d84726e
12 changed files with 231 additions and 121 deletions

View File

@ -54,7 +54,6 @@ export class UserService {
*
*/
async createArchiveTask(user: User, study: Study[]) {
console.log(user, study);
return await this.userRepository.createArchiveTask({ user, study });
}
}

View File

@ -0,0 +1,43 @@
import React from "react";
import { Modal, Select } from "antd";
interface AssignModalProps {
isOpen: boolean;
options: { value: unknown; label: unknown }[];
value: number | undefined;
onSelectAnnotator: (id: number) => void;
onConfirm: () => void;
onCancel: () => void;
}
const AssignModal: React.FC<AssignModalProps> = ({
isOpen,
options,
value,
onSelectAnnotator,
onConfirm,
onCancel,
}) => {
return (
<Modal
width={320}
title="选择标注"
open={isOpen}
closable
cancelText="再想想"
okText="确定"
onOk={onConfirm}
onCancel={onCancel}
>
<Select
style={{ width: "100%", marginBottom: 20 }}
placeholder="选择标注"
value={value}
onChange={(id) => onSelectAnnotator(id)}
options={options}
/>
</Modal>
);
};
export default AssignModal;

View File

@ -0,0 +1,25 @@
import { Typography, Space, Divider } from "antd";
import React from "react";
interface DicomFileInfoProps {
totalFileNum: string | number;
dcmFileNum: string | number;
dcmFileSize: string | number;
}
const { Text } = Typography;
export const DicomFileInfo: React.FC<DicomFileInfoProps> = ({
totalFileNum,
dcmFileNum,
dcmFileSize,
}) =>
!!totalFileNum && (
<Space>
<Text type="secondary">: {totalFileNum}</Text>
<Divider type="vertical" />
<Text type="secondary">: {dcmFileNum} dicom文件</Text>
<Divider type="vertical" />
<Text type="secondary">dcm文件总体积: {dcmFileSize} </Text>
</Space>
);

View File

@ -2,60 +2,60 @@ import { TableColumnsType } from "antd";
import { Series, Study } from "../DicomUploader/util";
export const columnsForStudy: TableColumnsType<Study> = [
{ title: "病历号", dataIndex: "patientID", key: "patientID" },
{ title: "姓名", dataIndex: "patientName", key: "patientName" },
{ title: "病历号", dataIndex: "PatientID", key: "PatientID" },
{ title: "姓名", dataIndex: "PatientName", key: "PatientName" },
{
title: "序列数",
key: "seriesNumber",
key: "SeriesNumber",
render: (_: any, record: Study) => <span>{record.subs.length}</span>,
},
{
title: "切片数量",
key: "seriesNumber",
key: "SliceNumber",
render: (_: any, record: Study) => (
<span>
{record.subs.map((s) => s.subs.length).reduce((p, n) => p + n)}
</span>
),
},
{ title: "年龄", dataIndex: "patientAge", key: "patientAge" },
{ title: "性别", dataIndex: "patientSex", key: "patientSex" },
{ title: "年龄", dataIndex: "PatientAge", key: "PatientAge" },
{ title: "性别", dataIndex: "PatientSex", key: "PatientSex" },
{
title: "扫描日期",
dataIndex: "acquisitionDate",
key: "acquisitionDate",
dataIndex: "AcquisitionDate",
key: "AcquisitionDate",
},
{
title: "描述",
dataIndex: "studyDescription",
key: "studyDescription",
dataIndex: "StudyDescription",
key: "StudyDescription",
},
];
export const columnsForSeries: TableColumnsType<Series> = [
{
title: "序列描述",
dataIndex: "seriesDescription",
key: "seriesDescription",
dataIndex: "SeriesDescription",
key: "SeriesDescription",
},
{
title: "切片数量",
key: "sliceNumber",
key: "SliceNumber",
render: (_: any, record: Series) => <span>{record.subs.length}</span>,
},
{
title: "切片层厚",
dataIndex: "sliceThickness",
key: "sliceThickness",
dataIndex: "SliceThickness",
key: "SliceThickness",
},
{
title: "成像设备",
dataIndex: "modality",
key: "modality",
dataIndex: "Modality",
key: "Modality",
},
{
title: "设备制造商",
dataIndex: "manufacturer",
key: "manufacturer",
dataIndex: "Manufacturer",
key: "Manufacturer",
},
];

View File

@ -2,27 +2,27 @@ import * as dicomParser from "dicom-parser";
export interface Series {
SeriesInstanceUID: string;
sliceThickness: string;
seriesDescription: string;
SliceThickness: string;
SeriesDescription: string;
/**
*
*/
modality: string;
Modality: string;
/**
*
*/
manufacturer: string;
Manufacturer: string;
subs: File[];
}
export interface Study {
StudyInstanceUID: string;
studyDescription: string;
patientID: string;
patientAge: string;
patientName: string;
patientSex: string;
acquisitionDate: string;
StudyDescription: string;
PatientID: string;
PatientAge: string;
PatientName: string;
PatientSex: string;
AcquisitionDate: string;
subs: Series[];
}
@ -38,31 +38,31 @@ export const parseDcmFiles = async (dcmFiles: File[]): Promise<Study[]> => {
const byteArray = new Uint8Array(arrayBuffer);
const dataSet = dicomParser.parseDicom(byteArray);
const patientID = dataSet.string("x00100020") ?? "无";
const PatientID = dataSet.string("x00100020") ?? "无";
const StudyInstanceUID = dataSet.string("x0020000d");
const studyDescription = dataSet.string("x00081030") ?? "无";
const StudyDescription = dataSet.string("x00081030") ?? "无";
const SeriesInstanceUID = dataSet.string("x0020000e");
const patientName = dataSet.string("x00100010") ?? "无";
const patientSex = dataSet.string("x00100040") ?? "无";
const patientAge = dataSet.string("x00101010") ?? "无";
const sliceThickness = dataSet.string("x00180050");
const acquisitionDate = dataSet.string("x00080022") ?? "无";
const seriesDescription = dataSet.string("x0008103e") ?? "无";
const modality = dataSet.string("x00080060") ?? "无";
const manufacturer = dataSet.string("x00081090") ?? "无";
const PatientName = dataSet.string("x00100010") ?? "无";
const PatientSex = dataSet.string("x00100040") ?? "无";
const PatientAge = dataSet.string("x00101010") ?? "无";
const SliceThickness = dataSet.string("x00180050");
const AcquisitionDate = dataSet.string("x00080022") ?? "无";
const SeriesDescription = dataSet.string("x0008103e") ?? "无";
const Modality = dataSet.string("x00080060") ?? "无";
const Manufacturer = dataSet.string("x00081090") ?? "无";
if (StudyInstanceUID && SeriesInstanceUID && sliceThickness) {
if (StudyInstanceUID && SeriesInstanceUID && SliceThickness) {
// 病例级别
let study = studys.find((s) => s.StudyInstanceUID === StudyInstanceUID);
if (!study) {
study = {
StudyInstanceUID,
studyDescription,
patientID,
patientName,
patientSex,
patientAge,
acquisitionDate,
StudyDescription,
PatientID,
PatientName,
PatientSex,
PatientAge,
AcquisitionDate,
subs: [],
};
studys.push(study);
@ -74,10 +74,10 @@ export const parseDcmFiles = async (dcmFiles: File[]): Promise<Study[]> => {
if (!series) {
series = {
SeriesInstanceUID,
sliceThickness,
seriesDescription,
modality,
manufacturer,
SliceThickness,
SeriesDescription,
Modality,
Manufacturer,
subs: [],
};
study.subs.push(series);

View File

@ -0,0 +1,20 @@
import React from "react";
import { Modal, Progress } from "antd";
interface UploadProgressProps {
title: string;
isOpen: boolean;
percent: number;
}
export const UploadProgressModal: React.FC<UploadProgressProps> = ({
isOpen,
percent,
title,
}) => {
return (
<Modal title={title} open={isOpen} footer={null} closable={false}>
<Progress percent={percent} strokeLinecap="butt" />
</Modal>
);
};

View File

@ -1,26 +1,16 @@
import {
Button,
Col,
Divider,
Modal,
Progress,
Row,
Select,
Space,
message,
} from "antd";
import { Button, Col, Row, Space, message } from "antd";
import { useDicomUploader } from "./DicomUploader";
import { Series, Study } from "./DicomUploader/util";
import { DicomTable } from "./DicomTable";
import { useDomain } from "@/hook/useDomain";
import { Typography } from "antd";
import { useState } from "react";
import { User } from "@@/domain/User/entities/User";
import { limitConcurrency } from "./limitConcurrency";
import { CloudUploadOutlined, InboxOutlined } from "@ant-design/icons";
import { openOHIFViewer, flatternStudies, FlatStudyItem } from "./util";
const { Text } = Typography;
import { DicomFileInfo } from "./DicomFileInfo";
import { UploadProgressModal } from "./UploadProgressModal";
import AssignModal from "./AssignModal";
interface DicomUploadProps {
children?: JSX.Element;
@ -33,6 +23,7 @@ export const DicomUpload = (props: DicomUploadProps) => {
const { dcmFileNum, totalFileNum, dcmFileSize } = fileCalculator;
const [selectRows, setSelectedRows] = useState<Study[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isAssignLoading, setIsAssignLoading] = useState(false);
const [annotators, setAnnotators] = useState<User[]>([]);
const [selectAnnotator, setSelectAnnotator] = useState<User | undefined>(
undefined
@ -72,9 +63,9 @@ export const DicomUpload = (props: DicomUploadProps) => {
};
/**
*
* pacs中的影像数据
*/
const onUploadEntiredDicom = () => {
const filterDicomPromise = () => {
const items = flatternStudies(studys);
// 过滤存在pacs中的序列
const checkExistPromises = items.map(
@ -88,8 +79,15 @@ export const DicomUpload = (props: DicomUploadProps) => {
resolve(existInPacs ? false : item);
})
) as Promise<FlatStudyItem | false>[];
return Promise.all(checkExistPromises);
};
/**
*
*/
const onUploadEntiredDicom = () => {
// 上传到pacs
Promise.all(checkExistPromises).then((result) => {
filterDicomPromise().then((result) => {
const files = result.map((i) => (!i ? [] : i.Files)).flat();
if (files.length === 0) return messageApi.info("全部影像均已存在pacs中");
setVisible(true);
@ -109,26 +107,22 @@ export const DicomUpload = (props: DicomUploadProps) => {
};
const onClickAssign = () => {
// 检查是否全部传到pacs
setIsAssignLoading(true);
filterDicomPromise().then((result) => {
const files = result.map((i) => (!i ? [] : i.Files)).flat();
if (files.length !== 0) {
setIsAssignLoading(false);
return messageApi.info("请先上传全部影像到pacs");
}
userDomainService.getDmpAnnotators().then((res) => {
setIsAssignLoading(false);
setIsModalOpen(true);
setAnnotators(res.data as User[]);
});
});
};
/**
*
*/
const DicomFileInfo = () =>
!!totalFileNum && (
<Space>
<Text type="secondary">: {totalFileNum}</Text>
<Divider type="vertical" />
<Text type="secondary">: {dcmFileNum} dicom文件</Text>
<Divider type="vertical" />
<Text type="secondary">dcm文件总体积: {dcmFileSize} </Text>
</Space>
);
/**
*
*/
@ -157,6 +151,7 @@ export const DicomUpload = (props: DicomUploadProps) => {
icon={<InboxOutlined />}
disabled={selectRows.length === 0}
type="primary"
loading={isAssignLoading}
onClick={onClickAssign}
>
@ -164,39 +159,35 @@ export const DicomUpload = (props: DicomUploadProps) => {
</Space>
</Col>
</Row>
<DicomFileInfo />
<DicomFileInfo
dcmFileNum={dcmFileNum}
dcmFileSize={dcmFileSize}
totalFileNum={totalFileNum}
/>
<DicomTable
studys={studys}
loading={isLoading}
onSelectedRows={(rows) => setSelectedRows(rows)}
onUploadFiles={onUploadFiles}
/>
<Modal
width={320}
title="选择标注"
open={isModalOpen}
closable
cancelText="再想想"
okText="确定"
onOk={onAssignConfirm}
onCancel={() => setIsModalOpen(false)}
>
<Select
style={{ width: "100%", marginBottom: 20 }}
placeholder="选择标注"
value={selectAnnotator?.id as number}
onChange={(id: number) =>
setSelectAnnotator(annotators.find((a) => a.id === id))
}
<AssignModal
isOpen={isModalOpen}
options={annotators.map((a) => ({
value: a.id,
label: a.username,
}))}
value={selectAnnotator?.id as number}
onSelectAnnotator={(id: number) =>
setSelectAnnotator(annotators.find((a) => a.id === id))
}
onConfirm={onAssignConfirm}
onCancel={() => setIsModalOpen(false)}
/>
<UploadProgressModal
title="上传影像中"
isOpen={visible}
percent={percent}
/>
</Modal>
<Modal title="上传影像中" open={visible} footer={null} closable={false}>
<Progress percent={percent} strokeLinecap="butt" />
</Modal>
{contextHolder}
</div>
);

View File

@ -8,8 +8,6 @@ export class AppController {
@EventPattern({ cmd: 'archive.task.create' })
async createArchiveTask(payload) {
const { annotatorId, study } = payload;
console.log(study);
return 123;
return await this.appService.createArchiveTask(payload);
}
}

View File

@ -4,7 +4,7 @@ import { AppService } from './app.service';
import { NacosModule } from './nacos/nacos.module';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ArchiveTask } from './entity/archiveTask';
import { ArchiveTask } from './entity/archiveTask.entity';
@Module({
imports: [
@ -19,7 +19,7 @@ import { ArchiveTask } from './entity/archiveTask';
port: 3306,
username: 'root',
password: 'root',
database: 'dicom',
database: 'dmp',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
timezone: 'Asia/Shanghai', // 这里设置了时区

View File

@ -1,4 +1,32 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ArchiveTask } from './entity/archiveTask.entity';
import { Repository } from 'typeorm';
@Injectable()
export class AppService {}
export class AppService {
constructor(
@InjectRepository(ArchiveTask)
private readonly archiveTaskRepository: Repository<ArchiveTask>,
) {}
async createArchiveTask(payload) {
const { annotatorId, study } = payload;
try {
for (let i = 0; i < study.length; i++) {
const { StudyInstanceUID, PatientID } = study[i];
for (let j = 0; j < study[i].subs.length; j++) {
const { SeriesInstanceUID } = study[i].subs[j];
await this.archiveTaskRepository.save({
annotatorId,
PatientID,
StudyInstanceUID,
SeriesInstanceUID,
});
}
}
return { success: true };
} catch (error) {
return { success: false, error };
}
}
}

View File

@ -3,9 +3,12 @@ import {
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
Unique,
UpdateDateColumn,
} from 'typeorm';
@Entity()
@Unique(['annotatorId', 'StudyInstanceUID', 'SeriesInstanceUID'])
export class ArchiveTask {
@PrimaryGeneratedColumn()
id: number;
@ -14,7 +17,10 @@ export class ArchiveTask {
annotatorId: number;
@Column()
StudyInstanceUID;
PatientID: string;
@Column()
StudyInstanceUID: string;
@Column()
SeriesInstanceUID: string;

View File

@ -17,10 +17,10 @@ export class AdminController {
@Post('createArchiveTask')
async createArchiveTask(@Body() body) {
const { user, study } = body;
const { annotatorId } = user;
const { data } = await firstValueFrom(
const { id: annotatorId } = user;
const { success, error } = await firstValueFrom(
this.client.send({ cmd: 'archive.task.create' }, { annotatorId, study }),
);
return { data, code: 0 };
return success ? { code: 0 } : { code: 1, msg: error.code };
}
}