目录
1. 项目概述
2. 开发人员团队
3. 大致需求
4. 开发内容
4.1. 前端开发
4.1.1: 前端页面开发
4.1.2: 登录机制以及路由守卫的开发
4.1.3: 文件上传机制和保存机制
4.1.4: 消息传递机制'
4.2. 线程池开发
4.3. 在线调试
1. 项目概述
搭建一个基于深度学习的分析平台, 使研究人员能够使用先进的深度学习架构, 来进行多模态医学影像的分析. 在完成模型多模态交互的同时, 为医护人员提供快捷便利的医学影响参考平台, 减轻医生的工作压力.
本项目采用人机交互设计原则和用户体验研究,设计了直观、简洁、易用的界面,并提供了个性化的用户操作,以增强医生和患则对系统的接受度和使用意愿。
本项目能处理多种不同模态的医学影像数据,如MRI、CT、X光等,以及临床数据,如患者的病史、症状等。有效地整合不同模态的信息,提取更丰富和准确的特征,进一步提高诊断和治疗的效果。
本项目通过多轮对话交互提供更有效的医学影像分析结果,支持病灶分割、疾病分类、病变检测以及疾病进展监测,为医生和患者提供了更好的医疗服务。
本项目能够解决百姓日常生活中大多数的医学问答、健康科普、健康管理等相关问题,采用自然语言处理技术和深度学习算法,通过大模型训练方法,形成人工智能“医生大脑”。
2. 开发人员团队
5人(tg, wyx, wyt, cjy, xpk), 分别负责开发, 测试以及产品设计等等工作.
3. 大致需求
搭建一个基于深度学习的分析平台, 使研究人员能够使用先进的深度学习架构, 来进行多模态医学影像的分析.
4. 开发内容
4.1. 前端开发
前端使用react架构实现开发, 最终预计在服务器环境上完成部署, 下方的目录顺序并非实际开发顺序, 每个部分均有所更新.
4.1.1: 前端页面开发
前端页面使用react开发, 采用组件式开发模式:
分成如图所示的多个界面, 最终效果如图所示:
内部功能如图所示:
4.1.2: 登录机制以及路由守卫的开发
首先先创建一个组件用来保存登录状态并将状态传递给下方的每一个组件, 在组件包括了跟组件
import { createContext, useState } from 'react';
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [session, setSession]=useState('')
const [email, setUserEmail]=useState('')
const login = () => {
setIsLoggedIn(true);
};
const logout = () => {
setIsLoggedIn(false);
};
return (
//传递数据为登录信息, 登入和登出操作, 会话id, 用户邮箱, 以及对应的修改操作
<AuthContext.Provider value={{ isLoggedIn,login, logout,session,email,setUserEmail, setSession }}>
{children}
</AuthContext.Provider>
);
};
将组件状态传递给下方的每一个组件
import logo from './logo.svg';
import './App.css';
import Home from './pages/home';
import { BrowserRouter, Routes,Route } from 'react-router-dom';
import Login from './pages/login';
import About from './pages/about';
import { AuthProvider } from './Auth';
import ProtectedRouter from './components/ProtectRouter';
import Chat from './pages/chat';
import Register from './pages/register';
function App() {
return (
<AuthProvider>{/*根组件用来传递状态和登录信息*/}
<BrowserRouter>
<Routes>
<Route path='/' element={<Home/>} />
<Route path='/login' element={<Login/>} />
<Route path='/register' element={<Register/>} />
<Route path='/chat' element={<ProtectedRouter Component={()=><Chat/>}/>}/>
<Route path='/about' element={<ProtectedRouter Component={()=><About/>}/>}/>
</Routes>
</BrowserRouter>
</AuthProvider>
);
}
export default App;
在一些需要登录的路由上创建一个路由守卫组件, 该组件会检测到用户是否正在登录状态, 如果不是, 则会强制跳转到登录界面.
import { useContext } from 'react';
import { Navigate } from 'react-router-dom';
import { BrowserRouter, Routes,Route } from 'react-router-dom';
import About from '../pages/about';
import { AuthContext } from '../Auth';
const ProtectedRouter = ({Component}) => { //传进来的对象以及其他的属性
const { isLoggedIn } = useContext(AuthContext); //检查登录状态
return (
isLoggedIn ? <Component/> : <Navigate to={{ pathname: '/login' }} /> //如果已经登录了, 就展示情况
);
};
export default ProtectedRouter;
4.1.3: 文件上传机制和保存机制
在最开始的时候尝试过使用单独组件封装的方式来实现文件的上传,
import { Upload, message } from 'antd';
import React from 'react';
function getBase64(img, callback) {
const reader = new FileReader();
reader.addEventListener('load', () => callback(reader.result));
reader.readAsDataURL(img);
}
function beforeUpload(file) {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
if (!isJpgOrPng) {
message.error('You can only upload JPG/PNG file!');
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error('Image must smaller than 2MB!');
}
return isJpgOrPng && isLt2M;
}
class Avatar extends React.Component {
state = {
loading: false,
};
handleChange = info => {
if (info.file.status === 'uploading') {
this.setState({ loading: true });
return;
}
if (info.file.status === 'done') {
// Get this url from response in real world.
getBase64(info.file.originFileObj, imageUrl =>
this.setState({
imageUrl,
loading: false,
}),
);
}
};
render() {
const uploadButton = (
<div>
<div className="ant-upload-text">Upload</div>
</div>
);
const { imageUrl } = this.state;
return (
<Upload
name="avatar"
listType="picture-card"
className="avatar-uploader"
showUploadList={false}
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
beforeUpload={beforeUpload}
onChange={this.handleChange}
>
{imageUrl ? <img src={imageUrl} alt="avatar" style={{ width: '100%' }} /> : uploadButton}
</Upload>
);
}
}
export default Avatar
但是后续出现了很多问题, 所以改用原生来实现提交
{(!imgUploaded)
&&
//自定义一个文件输入框
<button style={{
border: 'solid 5px #284B63',
borderRadius: 100,
height: 60,
width: '100%',
color:'#284B63',
fontSize:20,
fontWeight: 700,
marginBottom:20
}} onClick={()=>chooseFile()}>choose File</button>
}
{/*文件上传按钮, 隐藏原始的文件输入, 然后自定义一个*/}
<input type="file" id="fileInput" accept="*" style={{margin:10,display:'none'}}/>
{/*图片展示*/}
{hasImg && <div style={{height:256,width:256,justifyContent:'center',alignContent:'center'}}>
<img id="previewImage" alt="Preview" style={
imgUploaded?
{
height:256,
width:256
}
:
{
height:218,
width:218
}
}></img>
</div>}
{/*文件发送到后端的按钮*/}
{hasImg && (!imgUploaded) && <button onClick={()=>sendImage()} style={{
border: 'solid 5px #284B63',
borderRadius: 100,
height: 60,
width: '100%',
color:'#284B63',
color:'#284B63',
fontSize:20,
fontWeight: 700,
marginTop:20
}}>
Init dialog
</button>}
</div>
为了修改原有的ui按钮, 我们将原本的提交机制给隐藏, 再通过js进行点击事件的模拟触发, 最终实现图片的上传和展示, 图片的命名方式为img+会话id
//文件上传对象
const fileInput = document.getElementById('fileInput');
//设置监听对象
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
const previewImage = document.getElementById('previewImage');
if (file) {
if (file.type.startsWith('image/')) {
setHasImg(true)
const reader = new FileReader();
reader.onload = (e) => {
// 读取完成后,数据URL会作为结果
previewImage.src = e.target.result;
};
// 以Data URL的形式读取文件
reader.readAsDataURL(file);
} else {
alert('please choose a image file!');
}
}
})
4.1.4: 消息传递机制'
消息传递大致为以下三种
指令序号 | 内容 |
0 | 启动对话并将会话id传递给前后端 |
1 | 停止会话 |
2 | 正常的通讯消息 |
3 | 传递图片 |
前端的websocket数据监听
const wsMessageHandler = (message) => {
const data = jsonToObject(message.data);
if (data.type === 0){ //后端返回一个登录状态
setId(data.id);
setSession(data.id) //设置登录状态,知道会话id
}
else if (data.type === 1) {
console.log('当前问诊已经结束');
}
else if (data.type === 2) {
console.log('模型消息回复为', String(data.message));
appendList(String(data.message)) //是这个的问题, 强制刷新消息
setDialogShow(false)
}
else if (data.type === 3) {
console.log('图片的存储地点为', data.url);
}
};
后端ws数据监听
if(conn){ //如果链接对象已经创建
work=conn.worker //获取线程链接
if(data.type===2) { //发送到ws的指令为2, 则把数据传下去
work.postMessage(data)
}else if(data.type===1){ //接收到1指令
console.log('会话'+data.id+'已经停止')
work.postMessage(data) //向线程发送停止信息
workers.delete(data.id) //从线程队列中删除会话
}else if(data.type===3){
// 将Base64编码的字符串转换为Buffer
const imageBuffer = Buffer.from(data.data.split(';base64,').pop(), 'base64');
// 指定要保存图像的文件路径和文件名
const imagePath = './imgs/img'+data.id+'.png';
// 将图像数据写入文件
fs.writeFile(imagePath, imageBuffer, (err) => {
if (err) {console.error('Error saving image:', err); return; }
});
// 将图片的保存地址传递给子进程
work.postMessage({...data,url:imagePath})
}
}
后端监听子线程的消息
worker.on('message', (message) => {
console.log(message)
conn=workers.get(message.id)
if(message.type===2){
conn.getWebSocket().send(objectToJson(message))
}else if(message.type===1){
workers.delete(message.id) //从维护队列中删除
conn.getWebSocket().send(objectToJson(message)) //将数据发送给前端
}else if(message.type===3){
conn.getWebSocket().send(objectToJson(message)) //将数据发送给前端
}
})
4.2. 线程池开发
为了更好的实现用户的隔离和资源管理, 我们使用线程池的方式来实现前端-后端-模型的交互, 再开始阶段, 我们使用脚本来代替模型的功能
# 导入模块
import sys
import time
# 循环接收用户输入并进行对话,直到接收到指令退出
# 模拟的是一个模型的功能
time.sleep(5)
sys.stdout.buffer.write("你好".encode('utf-8'))
while True:
# 接收用户输入, 这里是一个组合方法
user_input = input()
# 如果用户输入指令 "exit",则退出循环
if user_input.strip() == "exit":
print("Exiting conversation...")
break
# 模拟数据的发送
time.sleep(5)
# python的回答, 也就是我们所需要的诊断内容, 果然是把这一句直接ASCII化了
sys.stdout.buffer.write("模型输出".encode('utf-8'))
后端主页代码
const WebSocket = require('ws');
const { Worker } = require('worker_threads');
const {jsonToObject,objectToJson,generateUniqueId}=require('./tool.js')
const fs = require('fs');
// 创建 WebSocket 服务器, 该服务器监听的是3001端口
const wss = new WebSocket.Server({ port: 3001 });
//链接类,id, 和前端的链接ws, 和后端的链接worker
class Connection {
constructor(id, ws, worker) {
this.id = id;
this.ws = ws;
this.worker = worker;
}
getId() { return this.id; }
getWebSocket() { return this.ws; }
getWorker() { return this.worker;}
setWebSocket(ws) { this.ws = ws;}
setWorker(worker) { this.worker = worker;}
}
//进程管理对象队列(映射)
const workers=new Map();
// 监听每次创建一个新的链接, 就会
// 创建一个ws对象并且设置监听器
// 将其加入队列中
wss.on('connection', (ws) => {
//创建id
const id=generateUniqueId()
//生成链接对象并且创建监听
ws.addEventListener('open', () => {
console.log('WebSocket connection established');
});
//监听前端的信息
ws.addEventListener('message', (event) => {
data=jsonToObject(event.data)
conn=workers.get(data.id) //先判断该会话是否还存在, 如果不存在就不需要什么反应
if(conn){ //如果链接对象已经创建
work=conn.worker //获取线程链接
if(data.type===2) { //发送到ws的指令为2, 则把数据传下去
work.postMessage(data)
}else if(data.type===1){ //接收到1指令
console.log('会话'+data.id+'已经停止')
work.postMessage(data) //向线程发送停止信息
workers.delete(data.id) //从线程队列中删除会话
}else if(data.type===3){
// 将Base64编码的字符串转换为Buffer
const imageBuffer = Buffer.from(data.data.split(';base64,').pop(), 'base64');
// 指定要保存图像的文件路径和文件名
const imagePath = './imgs/img'+data.id+'.png';
// 将图像数据写入文件
fs.writeFile(imagePath, imageBuffer, (err) => {
if (err) {console.error('Error saving image:', err); return; }
});
// 将图片的保存地址传递给子进程
work.postMessage({...data,url:imagePath})
}
}
});
//将id, 链接对象和子线程组合存储, 其中ws代表前端, worker代表后端线程
ws.addEventListener('error', (error) => {
console.error('WebSocket error:', error);
});
ws.addEventListener('close', () => { //监听到链接关闭以后
console.log('用户页面已经关闭') //将ws直接删除掉
conn=workers.get(id)
if(conn){
workers.delete(id)
}
});
// 创建子线程
const worker=new Worker('./worker.js')
// 监听来自子线程的消息
worker.on('message', (message) => {
console.log(message)
conn=workers.get(message.id)
if(message.type===2){
conn.getWebSocket().send(objectToJson(message))
}else if(message.type===1){
workers.delete(message.id) //从维护队列中删除
conn.getWebSocket().send(objectToJson(message)) //将数据发送给前端
}else if(message.type===3){
conn.getWebSocket().send(objectToJson(message)) //将数据发送给前端
}
})
//前端链接, 线程链接, 以及id存储起来
workers.set(id, new Connection(id,ws,worker))
//发送给前端id信息
ws.send(objectToJson({type:0,id:id}))
//发送给子线程id信息
worker.postMessage(objectToJson({type:0, id:id}))
console.log('新增会话用户, 当前会话数目为', workers.size)
});
子线程管理代码
// worker.js
const { parentPort } = require('worker_threads');
const {jsonToObject,objectToJson}=require('./tool');
const { spawn } = require('child_process');
/*
主线程会根据worker.js中的代码创建一个子线程
创建子线程的同时, 启动一个python服务, 该服务在启动的时候, 就把图片数据和第一个prompt传递过去
*/
//问诊状态
let inquiried=false
//图片地址
let imgUrl=''
// python脚本执行对象
let pythonProcess;
//会议id
let id;
//开启问诊状态,参数为图片的地址
//传递的msg就是图片的地址================================================================
const startInquriy=(msg)=>{
//启动模型,
pythonProcess = spawn('python', ['./foot.py', msg, '请你帮我诊断这张图片' ], {
stdio: ['pipe', 'pipe', 'pipe'] ,
encoding: 'utf-8'
});
//为模型增加一个输出监听, 从此会开始监听模型的输出
pythonProcess.stdout.on('data', (data) => {
if(!inquiried){
//如果检测到这是第一次输出, 则开启问诊状态
inquiried=true
//先进行响应3,告知前端图片地址在什么地方
parentPort.postMessage({type:3, id:id, url:msg})
}
//响应2, 告知前端模型的意见
// 将Buffer对象转换为十六进制字符串,并使用正则表达式替换掉空格
const hexString = data.toString('hex').replace(/ /g, '');
console.log(Buffer.from(hexString, 'hex'))
// 将十六进制字符串转换为实际的字符串
const str = Buffer.from(hexString, 'hex').toString('utf-8');
console.log(str)
parentPort.postMessage({type:2, id:id, message:str})
});
}
// 向 Python 脚本发送数据, 参数为发送的数据
const sendDataToPython = (data) => {
pythonProcess.stdin.write(data + '\n'); // 在每条消息末尾加上换行符
};
// 监听主线程发送的消息
parentPort.on('message', (message) => {
data=message
//0指令, 给线程分一个id
if(data.type===0){
id = data.id;
}
//1指令, 线程停止
else if(data.type===1){
if(inquiried){
//停止python脚本
sendDataToPython('exit');
//停止线程
process.exit()
}
}
//2指令, 发来数据, 传回数据
else if(data.type===2){
//输入数据, 这个data.message其实就是前端传入进来的话
if(inquiried){
sendDataToPython(data.message);
}
}
//3指令, 开启对话
else if(data.type===3){
//输入图片, 并且确认开启python脚本
imgUrl = data.url
id = data.id
startInquriy(data.url);
}
});
4.3. 在线调试
在线调试的过程中遇到cors跨域的问题, 解决方案是使用http-server的包进行本地服务器建立并且运行, 在目录下进入启动http-server指令