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

View File

@ -2,27 +2,27 @@ import * as dicomParser from "dicom-parser";
export interface Series { export interface Series {
SeriesInstanceUID: string; SeriesInstanceUID: string;
sliceThickness: string; SliceThickness: string;
seriesDescription: string; SeriesDescription: string;
/** /**
* *
*/ */
modality: string; Modality: string;
/** /**
* *
*/ */
manufacturer: string; Manufacturer: string;
subs: File[]; subs: File[];
} }
export interface Study { export interface Study {
StudyInstanceUID: string; StudyInstanceUID: string;
studyDescription: string; StudyDescription: string;
patientID: string; PatientID: string;
patientAge: string; PatientAge: string;
patientName: string; PatientName: string;
patientSex: string; PatientSex: string;
acquisitionDate: string; AcquisitionDate: string;
subs: Series[]; subs: Series[];
} }
@ -38,31 +38,31 @@ export const parseDcmFiles = async (dcmFiles: File[]): Promise<Study[]> => {
const byteArray = new Uint8Array(arrayBuffer); const byteArray = new Uint8Array(arrayBuffer);
const dataSet = dicomParser.parseDicom(byteArray); const dataSet = dicomParser.parseDicom(byteArray);
const patientID = dataSet.string("x00100020") ?? "无"; const PatientID = dataSet.string("x00100020") ?? "无";
const StudyInstanceUID = dataSet.string("x0020000d"); const StudyInstanceUID = dataSet.string("x0020000d");
const studyDescription = dataSet.string("x00081030") ?? "无"; const StudyDescription = dataSet.string("x00081030") ?? "无";
const SeriesInstanceUID = dataSet.string("x0020000e"); const SeriesInstanceUID = dataSet.string("x0020000e");
const patientName = dataSet.string("x00100010") ?? "无"; const PatientName = dataSet.string("x00100010") ?? "无";
const patientSex = dataSet.string("x00100040") ?? "无"; const PatientSex = dataSet.string("x00100040") ?? "无";
const patientAge = dataSet.string("x00101010") ?? "无"; const PatientAge = dataSet.string("x00101010") ?? "无";
const sliceThickness = dataSet.string("x00180050"); const SliceThickness = dataSet.string("x00180050");
const acquisitionDate = dataSet.string("x00080022") ?? "无"; const AcquisitionDate = dataSet.string("x00080022") ?? "无";
const seriesDescription = dataSet.string("x0008103e") ?? "无"; const SeriesDescription = dataSet.string("x0008103e") ?? "无";
const modality = dataSet.string("x00080060") ?? "无"; const Modality = dataSet.string("x00080060") ?? "无";
const manufacturer = dataSet.string("x00081090") ?? "无"; const Manufacturer = dataSet.string("x00081090") ?? "无";
if (StudyInstanceUID && SeriesInstanceUID && sliceThickness) { if (StudyInstanceUID && SeriesInstanceUID && SliceThickness) {
// 病例级别 // 病例级别
let study = studys.find((s) => s.StudyInstanceUID === StudyInstanceUID); let study = studys.find((s) => s.StudyInstanceUID === StudyInstanceUID);
if (!study) { if (!study) {
study = { study = {
StudyInstanceUID, StudyInstanceUID,
studyDescription, StudyDescription,
patientID, PatientID,
patientName, PatientName,
patientSex, PatientSex,
patientAge, PatientAge,
acquisitionDate, AcquisitionDate,
subs: [], subs: [],
}; };
studys.push(study); studys.push(study);
@ -74,10 +74,10 @@ export const parseDcmFiles = async (dcmFiles: File[]): Promise<Study[]> => {
if (!series) { if (!series) {
series = { series = {
SeriesInstanceUID, SeriesInstanceUID,
sliceThickness, SliceThickness,
seriesDescription, SeriesDescription,
modality, Modality,
manufacturer, Manufacturer,
subs: [], subs: [],
}; };
study.subs.push(series); 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 { import { Button, Col, Row, Space, message } from "antd";
Button,
Col,
Divider,
Modal,
Progress,
Row,
Select,
Space,
message,
} from "antd";
import { useDicomUploader } from "./DicomUploader"; import { useDicomUploader } from "./DicomUploader";
import { Series, Study } from "./DicomUploader/util"; import { Series, Study } from "./DicomUploader/util";
import { DicomTable } from "./DicomTable"; import { DicomTable } from "./DicomTable";
import { useDomain } from "@/hook/useDomain"; import { useDomain } from "@/hook/useDomain";
import { Typography } from "antd";
import { useState } from "react"; import { useState } from "react";
import { User } from "@@/domain/User/entities/User"; import { User } from "@@/domain/User/entities/User";
import { limitConcurrency } from "./limitConcurrency"; import { limitConcurrency } from "./limitConcurrency";
import { CloudUploadOutlined, InboxOutlined } from "@ant-design/icons"; import { CloudUploadOutlined, InboxOutlined } from "@ant-design/icons";
import { openOHIFViewer, flatternStudies, FlatStudyItem } from "./util"; import { openOHIFViewer, flatternStudies, FlatStudyItem } from "./util";
import { DicomFileInfo } from "./DicomFileInfo";
const { Text } = Typography; import { UploadProgressModal } from "./UploadProgressModal";
import AssignModal from "./AssignModal";
interface DicomUploadProps { interface DicomUploadProps {
children?: JSX.Element; children?: JSX.Element;
@ -33,6 +23,7 @@ export const DicomUpload = (props: DicomUploadProps) => {
const { dcmFileNum, totalFileNum, dcmFileSize } = fileCalculator; const { dcmFileNum, totalFileNum, dcmFileSize } = fileCalculator;
const [selectRows, setSelectedRows] = useState<Study[]>([]); const [selectRows, setSelectedRows] = useState<Study[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [isAssignLoading, setIsAssignLoading] = useState(false);
const [annotators, setAnnotators] = useState<User[]>([]); const [annotators, setAnnotators] = useState<User[]>([]);
const [selectAnnotator, setSelectAnnotator] = useState<User | undefined>( const [selectAnnotator, setSelectAnnotator] = useState<User | undefined>(
undefined undefined
@ -72,9 +63,9 @@ export const DicomUpload = (props: DicomUploadProps) => {
}; };
/** /**
* * pacs中的影像数据
*/ */
const onUploadEntiredDicom = () => { const filterDicomPromise = () => {
const items = flatternStudies(studys); const items = flatternStudies(studys);
// 过滤存在pacs中的序列 // 过滤存在pacs中的序列
const checkExistPromises = items.map( const checkExistPromises = items.map(
@ -88,8 +79,15 @@ export const DicomUpload = (props: DicomUploadProps) => {
resolve(existInPacs ? false : item); resolve(existInPacs ? false : item);
}) })
) as Promise<FlatStudyItem | false>[]; ) as Promise<FlatStudyItem | false>[];
return Promise.all(checkExistPromises);
};
/**
*
*/
const onUploadEntiredDicom = () => {
// 上传到pacs // 上传到pacs
Promise.all(checkExistPromises).then((result) => { filterDicomPromise().then((result) => {
const files = result.map((i) => (!i ? [] : i.Files)).flat(); const files = result.map((i) => (!i ? [] : i.Files)).flat();
if (files.length === 0) return messageApi.info("全部影像均已存在pacs中"); if (files.length === 0) return messageApi.info("全部影像均已存在pacs中");
setVisible(true); setVisible(true);
@ -109,26 +107,22 @@ export const DicomUpload = (props: DicomUploadProps) => {
}; };
const onClickAssign = () => { 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) => { userDomainService.getDmpAnnotators().then((res) => {
setIsAssignLoading(false);
setIsModalOpen(true); setIsModalOpen(true);
setAnnotators(res.data as User[]); 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 />} icon={<InboxOutlined />}
disabled={selectRows.length === 0} disabled={selectRows.length === 0}
type="primary" type="primary"
loading={isAssignLoading}
onClick={onClickAssign} onClick={onClickAssign}
> >
@ -164,39 +159,35 @@ export const DicomUpload = (props: DicomUploadProps) => {
</Space> </Space>
</Col> </Col>
</Row> </Row>
<DicomFileInfo /> <DicomFileInfo
dcmFileNum={dcmFileNum}
dcmFileSize={dcmFileSize}
totalFileNum={totalFileNum}
/>
<DicomTable <DicomTable
studys={studys} studys={studys}
loading={isLoading} loading={isLoading}
onSelectedRows={(rows) => setSelectedRows(rows)} onSelectedRows={(rows) => setSelectedRows(rows)}
onUploadFiles={onUploadFiles} onUploadFiles={onUploadFiles}
/> />
<Modal <AssignModal
width={320} isOpen={isModalOpen}
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))
}
options={annotators.map((a) => ({ options={annotators.map((a) => ({
value: a.id, value: a.id,
label: a.username, 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} {contextHolder}
</div> </div>
); );

View File

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

View File

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

View File

@ -1,4 +1,32 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ArchiveTask } from './entity/archiveTask.entity';
import { Repository } from 'typeorm';
@Injectable() @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, CreateDateColumn,
Entity, Entity,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
Unique,
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
@Entity() @Entity()
@Unique(['annotatorId', 'StudyInstanceUID', 'SeriesInstanceUID'])
export class ArchiveTask { export class ArchiveTask {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@ -14,7 +17,10 @@ export class ArchiveTask {
annotatorId: number; annotatorId: number;
@Column() @Column()
StudyInstanceUID; PatientID: string;
@Column()
StudyInstanceUID: string;
@Column() @Column()
SeriesInstanceUID: string; SeriesInstanceUID: string;

View File

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