ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI+Web3D:用开源工具从医学影像构建浏览器端3D人体解剖模型

AI+Web3D:用开源工具从医学影像构建浏览器端3D人体解剖模型 最近开发者社区里那个 3D 人体可视化项目确实火公开传播数据已经到了百万级围观。它给人的第一反应是这和课本上的解剖图完全不是一个物种。传统解剖图是二维平面示意而这个项目把人体结构真正放进了三维空间用户可以旋转、放大、分层、剖切把皮肤去掉看肌肉把肌肉去掉看骨骼甚至单独抽出血管和神经来观察。很多人以为做这种东西需要医学背景、需要专业工作站、需要几十万买数据。实际上现在这条技术链路已经可以用开源工具完整拼出来核心就是两步AI 对医学影像做器官分割再把分割结果转成 3D 网格丢给 WebGL 渲染。这篇文章就按这条思路讲清楚一个普通开发者怎么用 AI 加 Web 3D 技术从合法影像数据出发手搓出一个能在浏览器里跑的 3D 人体解剖可视化工具。内容覆盖技术架构、环境准备、本地部署、功能测试、接口设计、批量任务、资源占用和问题排查。前半段讲清楚项目能不能做、门槛有多高后半段直接给实操路径。1. 核心能力速览先把这套方案的关键信息放在最前面方便读者快速判断自己是否需要继续往下看。能力项说明项目类型3D 人体解剖结构可视化工具技术链路AI 医学影像分割 Three.js WebGL 渲染数据输入合法授权的 CT/MRI 影像序列常见格式为 DICOM 或 NIfTI客户端要求支持 WebGL2 的现代浏览器如 Chrome、Edge、Firefox显卡要求网页浏览端不需要独立显卡本地跑 AI 分割建议使用 NVIDIA GPU显存需求取决于所选分割模型需以实际模型为准启动方式前端可静态托管可通过 npm 启动开发服务也可用 Docker 整体部署接口能力结构列表、结构搜索、模型文件获取、标注信息查询批量任务支持批量处理影像目录并导出 GLB/STL 文件适合场景解剖学教学、医学可视化 Demo、交互式科普、3D 打印教学模型主要局限3D 模型只能辅助理解不能作为临床诊断依据这里有一个容易被忽略的点这个项目的实时交互阶段并不吃 GPU 算力。用户打开网页旋转模型、切分层、看标注渲染压力由显卡驱动和浏览器承担普通办公本也能跑。真正的算力需求集中在“数据预处理 AI 分割”这个离线阶段而这一步不需要用户参与。2. 适用场景与使用边界2.1 适合什么人第一类人是医学相关专业的老师、学生和解剖学科普作者。二维解剖图的信息表达方式有限很多空间结构关系必须靠三维模型才能讲清楚比如肩关节周围肌肉的叠放关系、颅底血管的走行路径。第二类是 Web 3D 开发者这套项目是学习 Three.js 医疗可视化的绝佳载体。第三类是 3D 打印爱好者AI 分割出来的器官网格可以直接导出 STL做成物理模型用于教学演示。2.2 能解决什么问题它最大的价值是把“概念图”变成“可交互模型”。学生不用在脑子里硬补三维空间关系直接用鼠标拖拽就能看清每一个结构。对医患沟通场景来说医生可以在 3D 模型上指给患者看问题部位比口头描述准确得多。2.3 不适合什么场景这个工具不能用于临床诊断不能替代医生阅片也不能作为手术导航依据。AI 分割结果可能与影像科医生手动勾画存在误差尤其是在肿瘤边界、小血管这些复杂结构上。另外不要试图用真实患者数据直接做展示除非数据已经完成匿名化并且拿到了完整的合法授权。2.4 版权、隐私与安全边界医学影像数据属于高度敏感信息。训练和使用过程中必须确保数据来源合法不能使用未获得授权的 DICOM 文件。展示人体结构时如果涉及真实患者影像必须完成去隐私化处理去除姓名、住院号、检查日期等所有直接标识信息。做 3D 打印复刻器官模型之前也要确认原始数据授权范围是否覆盖实物制作。涉及人脸、可辨识身体特征或者任何个体隐私数据时必须严格限制使用范围。3. 技术架构与核心处理流程整套方案可以拆成六个阶段下面按顺序过一遍。3.1 数据准备输入是 CT 或 MRI 影像序列。CT 的骨骼和空气对比度高适合骨骼分割MRI 对软组织的分辨率更好适合肌肉、脏器分割。原始数据通常是 DICOM 格式单一个检查可能包含几百张切片需要先统一转成 NIfTI 这类单文件格式方便后续分析。这个阶段要重点检查数据方向是否一致不同扫描设备输出的坐标朝向可能不同会影响 AI 模型的分割结果。3.2 AI 器官分割这是整套流程里最“AI”的一步。开源社区里已经有成熟的医学影像分割模型比如 nnU-Net 系列和 TotalSegmentator。它们可以接收 NIfTI 格式的 CT 影像自动输出器官和组织的二值掩膜每个掩膜对应一个解剖结构。分割模型通常需要 GPU 推理输入 CT 影像后输出多个结构掩膜每个结构都是独立的。这一步非常关键因为后续所有 3D 网格都是从这些掩膜里提取出来的。3.3 网格提取拿到掩膜之后用 Marching Cubes 算法从体素掩膜中提取三角形网格。这个过程可以使用 trimesh、PyMCubes 或医学影像软件完成。但直接提取出的网格面数通常很高动辄几百万三角形不适合直接丢给浏览器渲染必须做减面处理。常见的做法是先用网格简化算法把面数降到 10 万到 30 万再导出为 GLTF 或 GLB 格式。3.4 Web 端 3D 渲染浏览器端使用 Three.js 加载 GLB 模型通过 OrbitControls 实现旋转、缩放和平移。每个解剖结构独立注册成一个 Mesh用户可以通过 UI 控制每个 Mesh 的可见性和透明度。透明度的实现方式是调整材质 opacity 值并把渲染模式设置为 transparent。如果想做剖切效果可以通过 Three.js 的 clippingPlanes 来实现。3.5 交互功能完成基础渲染后可以叠加更多交互功能鼠标悬停高亮结构、点击弹出结构名称和说明、搜索框定位结构、下拉菜单切换显示层级、剖切面拖动杆、测量工具等。这些功能会让演示效果接近商业级医疗可视化软件。3.6 部署与扩展最后把前端页面部署到静态托管服务或者用 Docker 把前端加后端 API 一起部署。如果需要提供接口给其他系统调用可以加一层 FastAPI 或 Flask 后端负责返回结构列表、模型文件地址和标注信息。下面是一段典型的 AI 分割调用示意实际使用时需要按所选模型调整参数# 以 nnU-Net / TotalSegmentator 类模型为例 import os import subprocess # 输入目录DICOM 转换后的 NIfTI 文件 # 输出目录分割掩膜保存为 NIfTI input_nifti ./data/patient_001_ct.nii.gz output_dir ./output/masks # 注意实际命令由所选模型决定这里只是流程示意 cmd [ totalsegmentator, -i, input_nifti, -o, output_dir, --task, total, ] result subprocess.run(cmd, capture_outputTrue, textTrue) if result.returncode ! 0: print(分割失败, result.stderr) else: print(分割完成输出目录, output_dir)4. 环境准备与前置条件在开始搭建之前先确认本机环境是否满足基本条件。这套方案对开发者的环境要求不复杂但每一条都值得提前检查避免装到一半发现缺东西。4.1 操作系统Windows、Linux、macOS 都可以。前端部分完全跨平台AI 分割部分在 Windows 和 Linux 上体验最流畅。macOS 如果有 Apple Silicon 芯片也能跑很大一部分分割模型但部分医学影像库的 GPU 支持需要单独确认。4.2 运行环境前端需要 Node.js 18 或更高版本推荐使用 20 LTS。AI 分割管线需要 Python 3.10 或更高版本并安装 PyTorch、SimpleITK、torchio、trimesh 等常用库。如果只需要跑前端查看器不跑 AI 分割甚至可以不用 Python 环境。4.3 GPU 与显存网页渲染阶段不要求独立显卡UHD 核显配合 Chrome 也能流畅展示中低面数模型。AI 分割阶段建议使用 NVIDIA GPU因为多数预训练模型对 CUDA 支持最好。显存需求没法给一个固定数字不同模型、不同分辨率差异很大。稳妥的做法是先准备一张 8G 显存的显卡然后用小数据量跑一次看“显存占用率”再决定是否升级。4.4 磁盘空间原始 DICOM 序列解压后可能占用 500MB 到 2GB。中间产物包括 NIfTI 文件、分割掩膜、提取出的 OBJ 网格可能再占 1 到 3GB。导出压缩后的 GLB 文件通常在 50MB 到 300MB 之间取决于面数。规划项目目录时建议预留至少 20GB 空间。4.5 浏览器必须支持 WebGL2。Chrome 88 以上、Edge 88 以上、Firefox 79 以上都支持。如果打开页面黑屏第一优先检查 WebGL 是否被浏览器关闭尤其是部分旧电脑的集成显卡会默认禁用硬件加速。4.6 端口前端开发服务器默认监听 5173Vite 项目或 3000Node 项目后端 API 默认监听 8000。如果本机端口被占用可以使用下面的命令查找占用进程并换端口# Linux / macOS 查看端口占用 lsof -i :5173 # Windows PowerShell 查看端口占用 netstat -ano | findstr :51735. 本地部署与启动方式部署分为三个部分前端查看器、后端 API、AI 分割脚本。下面分别给出启动方式。5.1 前端查看器启动以 Vite Three.js 项目为例。项目名可以叫 anatomy3d-web目录结构大致如下anatomy3d-web/ ├── public/models/ │ └── human_glb/ ├── src/ │ ├── main.js │ ├── viewer.js │ └── api.js ├── index.html ├── package.json └── vite.config.js在项目根目录执行# 安装依赖 npm install # 启动开发服务器 npm run dev启动后浏览器访问终端输出的地址一般是http://127.0.0.1:5173。如果页面能加载并显示 3D 场景说明前端环境正常。生产环境构建和预览npm run build npm run preview5.2 前端加载 GLB 模型示例下面是使用 Three.js 加载 GLB 模型的基础代码。这段代码是通用的可以直接复制到自己的前端项目中修改路径import * as THREE from three; import { OrbitControls } from three/examples/jsm/controls/OrbitControls.js; import { GLTFLoader } from three/examples/jsm/loaders/GLTFLoader.js; const scene new THREE.Scene(); const camera new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 0, 50); const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setClearColor(0x0a0a0a); document.body.appendChild(renderer.domElement); const controls new OrbitControls(camera, renderer.domElement); controls.enableDamping true; scene.add(new THREE.AmbientLight(0xffffff, 0.6)); const dirLight new THREE.DirectionalLight(0xffffff, 1); dirLight.position.set(20, 30, 40); scene.add(dirLight); const loader new GLTFLoader(); loader.load(/models/human_glb/skeleton.glb, (gltf) { scene.add(gltf.scene); }); function animate() { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); } animate();如果模型加载不显示优先检查两点模型路径是否正确、模型文件是否为有效 GLB。浏览器控制台一般会给出明确报错。5.3 后端 API 启动如果要做结构搜索、标注查询需要启动一个轻量后端。这里用 FastAPI 为例from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel app FastAPI() app.add_middleware( CORSMiddleware, allow_origins[*], allow_methods[*], allow_headers[*], ) # 解剖结构索引实际应从数据库或 JSON 文件读取 STRUCTURES { skeleton: {name: 骨骼, category: 系统, model: /models/skeleton.glb}, heart: {name: 心脏, category: 器官, model: /models/heart.glb}, brain: {name: 脑, category: 器官, model: /models/brain.glb}, } app.get(/api/structures) def list_structures(): return {total: len(STRUCTURES), items: STRUCTURES} app.get(/api/search) def search_structures(q: str ): q q.lower() result {k: v for k, v in STRUCTURES.items() if q in k or q in v[name]} return {items: result}启动命令uvicorn main:app --host 0.0.0.0 --port 8000启动后用浏览器访问http://127.0.0.1:8000/docs可以打开自动生成的接口测试页面。5.4 Docker 整体部署如果不想分别启动前端和后端可以用 Docker Compose 一次拉起。下面是一个示意配置version: 3.8 services: backend: build: ./backend ports: - 8000:8000 volumes: - ./models:/models restart: unless-stopped frontend: build: ./frontend ports: - 5173:80 depends_on: - backend restart: unless-stopped构建和启动docker compose up -d镜像构建过程中如果网络不稳定可以给 Docker 配置镜像加速。启动后访问http://127.0.0.1:5173即可看到页面。Docker 部署适合给团队内部演示用不用每台电脑都配 Node 环境。6. 功能测试与效果验证部署完成之后按下面的维度逐项验证功能是否正常。这套验证流程不依赖具体项目代码适用于绝大多数 3D 医学可视化应用。6.1 基础加载测试测试目标确认 GLB 模型能正常加载并显示。操作方式启动前端服务打开页面等待模型出现。判断标准页面无 JS 报错场景中出现完整模型鼠标拖拽可以旋转滚轮可以缩放。失败时优先检查模型路径、格式是否有效、浏览器控制台是否有 404 报错。6.2 分层显示测试测试目标确认多个解剖结构可以独立控制显示与隐藏。操作方式在 UI 中切换皮肤层、骨骼层、血管层。判断标准关闭骨骼层后骨骼消失血管层开启后血管以半透明状态显示。这一项最需要观察透明度变化是否平滑如果开启透明度后模型叠影严重需要调整渲染顺序和 depthWrite 设置。6.3 剖切测试测试目标确认剖切面能正确切割模型帮助观察内部结构。操作方式调用 Three.js clippingPlanes设置一个可拖动的平面滑块。判断标准拖动滑块时模型被平面整洁地切开切口处能透出内部结构。常见问题剖切平面不平整、剖切后背面丢失、部分结构被错误裁切。6.4 标签与搜索测试测试目标确认结构名称和标注信息能正常显示。操作方式点击某个器官弹出名称和简要说明在搜索框输入“心”或“heart”。判断标准点击能选中对应结构搜索能返回匹配结果并自动定位视角。这里最容易出问题的是模型内部节点 ID 和语义标签对不上需要在导出模型时保留结构命名。6.5 高分辨率与多结构压测这是最容易暴露性能问题的一步。把骨骼、肌肉、血管、神经同时打开旋转视角观察帧率。如果旋转明显卡顿说明模型面数过高。此时可以把单个结构的面数降到 20 万以下或者用 Draco 压缩把 GLB 文件体积缩小 60% 以上。6.6 AI 分割效果验证对于自己跑分割的开发者重点检查每个器官的掩膜是否完整、是否存在粘连或缺失。判断方式把分割掩膜叠加到原始 CT 切片上看边界是否贴合。如果骨骼和肌肉粘连严重可以调整分割阈值如果小血管大量断裂可能需要提高模型分辨率和显存。分割效果直接决定 3D 模型的可用性这一步值得多花时间。7. 接口 API 与批量任务接口设计决定了这套方案能不能作为基础设施提供给其他系统用。推荐提供下面几种类型的接口。7.1 接口列表接口路由说明获取结构列表GET /api/structures返回所有解剖结构的索引搜索结构GET /api/search?qheart按名称搜索结构获取模型文件GET /models/{structure_id}.glb返回对应结构的 GLB 模型获取标注信息GET /api/annotations/{structure_id}返回结构的描述文字健康检查GET /api/health检查服务是否正常7.2 接口调用示例下面是前端调用接口获取结构列表的示例curl -X GET http://127.0.0.1:8000/api/structures \ -H Content-Type: application/json返回内容{ total: 3, items: { skeleton: { name: 骨骼, category: 系统, model: /models/skeleton.glb } } }7.3 Python 调用完整示例import requests base_url http://127.0.0.1:8000 # 1. 获取结构列表 resp requests.get(f{base_url}/api/structures, timeout10) data resp.json() print(结构数量:, data[total]) # 2. 搜索结构 search_resp requests.get(f{base_url}/api/search, params{q: heart}, timeout10) print(搜索结果:, search_resp.json()) # 3. 下载模型文件 model_resp requests.get(f{base_url}/models/skeleton.glb, timeout60) if model_resp.status_code 200: with open(skeleton.glb, wb) as fp: fp.write(model_resp.content) print(模型下载完成) else: print(模型下载失败状态码:, model_resp.status_code)7.4 批量任务设计如果有一整个影像目录需要处理推荐把流程拆成“扫描目录 → 格式转换 → AI 分割 → 网格提取 → 导出 GLB”五个步骤每个步骤独立执行并写日志。批量任务最容易踩的坑是单个样本失败导致整个流程中断所以每处理完一个样本都要记录状态失败样本单独标记不阻断后续任务。下面是一个可参考的批量处理伪代码流程import os import glob import json from pathlib import Path input_root Path(./data/ct_scans) output_root Path(./output/glb_models) log_file Path(./output/batch_log.jsonl) nifti_files sorted(glob.glob(str(input_root / ** / *.nii.gz), recursiveTrue)) for nifti_path in nifti_files: sample_name Path(nifti_path).stem.replace(.nii, ) out_dir output_root / sample_name out_dir.mkdir(parentsTrue, exist_okTrue) try: # 1. 调用 AI 分割模型生成 mask # 2. 从 mask 提取网格并减面 # 3. 导出 GLB 文件到 out_dir print(f[OK] {sample_name}) except Exception as e: print(f[FAIL] {sample_name}: {e}) # 每次处理完立即写入日志 with open(log_file, a, encodingutf-8) as fp: fp.write(json.dumps({sample: sample_name, status: ok}) \n)批量任务建议加上以下设计单样本超时设定、失败重试两次、输出条数校验、中间产物定期清理。8. 资源占用与性能观察性能观察是这类项目的重头。先说结论浏览器端资源占用主要取决于模型面数、材质数量和透明度开启比例AI 分割阶段资源占用主要取决于模型参数量和输入影像分辨率。8.1 浏览器端性能观察打开浏览器开发者工具切到 Performance 面板点击录制后旋转模型。重点看两个指标GPU 进程占用率和主线程帧时间。如果 GPU 占用冲到 90% 以上说明渲染压力大需要减面如果主线程帧时间经常超过 50ms说明 JS 逻辑有优化空间比如场景遍历、标注计算等操作过于频繁。8.2 显存占用观察AI 分割期间用命令观察 GPU 显存nvidia-smi -l 1如果显存占用接近上限减小输入影像分辨率或者降低 batch size。注意分割模型的显存占用会随输入尺寸翻好几倍一定要从最小的输入尺寸开始试。8.3 降低资源占用的手段模型端可以做的使用 Draco 压缩 GLB 文件减小解码后 GPU 传输压力为低配设备准备低面数 LOD 版本把透明材质数量控制在同一时间不超过 5 个。代码端可以做的不在每一帧创建新对象复用 Geometry 和 Material用一个 DrawCall 合并静态结构减少渲染批次。9. 常见问题与排查方法实践过程中大概率会遇到下面这些问题直接给出排查清单。问题现象可能原因排查方式解决方案页面打开黑屏浏览器不支持 WebGL 或硬件加速被禁用打开 Chrome 地址栏输入 chrome://gpu 查看 WebGL 状态启用硬件加速或更换浏览器模型加载后不显示GLB 路径错误或文件损坏查看浏览器控制台是否有 404修正路径或重新导出 GLB模型面数过高导致卡顿网格没有减面或简化参数过大观察帧率和 DrawCall 数量使用减面工具把单结构面数降到 20 万以下器官分割结果粘连影像分辨率或分割阈值不匹配叠加查看原始影像和掩膜调整阈值或重跑分割部分结构透明后出现叠影材质顺序和 depthWrite 设置不对对比开启透明前后的渲染效果调整渲染队列和 depthWrite 状态接口跨域请求失败CORS 未配置查看浏览器控制台 CORS 报错后端配置跨域中间件限制可用来源端口被占用上一个服务未退出使用 lsof 或 netstat 查端口结束进程或换端口启动批量任务中途卡住单样本推理时间过长查看日志定位卡住阶段增加超时和失败重试机制Docker 内访问不到模型模型文件没有挂载进容器检查 Docker volumes 配置把模型目录挂载到容器内对应路径10. 最佳实践与使用建议从零到一跑通这套系统之后建议再做几件事让项目具备可持续维护的基础。第一把原始影像、中间 mask、减面网格、最终 GLB 文件分开目录管理。不要把中间产物覆盖原始数据AI 分割参数经常要调保留每一版中间结果是回滚的前提。第二保留一套最小可运行配置。建议维护一个config.example.json记录输入路径、输出路径、模型名称、面数上限、透明度默认值等参数。换机器、换数据时直接套用这套配置能省去很多重复踩坑时间。第三批量任务必须写日志。每个样本都要记录状态、耗时、占用显存和失败原因。没有日志的批量任务就像没有仪表盘的飞机出了问题只能从头开始查。第四接口服务要限制访问范围。如果后端 API 暴露在公网必须加入访问控制或认证机制否则任何人都可以遍历你的数据接口。开发阶段建议绑定127.0.0.1生产环境再考虑内网部署或反向代理。第五涉及真实医学影像、人脸、声音等敏感素材时上线之前一定要做授权复核。确认数据来源、匿名化程度、展示范围和商用边界尤其是 3D 打印实物这一类可能超出数字展示范畴的用途。不要因为演示效果好看就忽略合规问题。11. 总结与下一步这个项目最值得尝试的地方在于它把抽象的解剖学知识变成了可交互的三维体验。对于一个普通开发者来说技术门槛并没有想象中那么高AI 分割有开源模型Web 渲染有 Three.js二者组合起来就能做出接近商业医学可视化软件的效果。最先应该验证的功能是 AI 分割的准确度。拿一个小样本数据跑一次看分割 mask 是否完整、边界是否干净这决定了后续所有模型质量。最容易踩的坑则是模型面数过高导致浏览器卡顿以及数据隐私授权没有提前确认。后续可以扩展的方向很多接入大模型做解剖知识问答加入语音控制让老师在演示时解放双手导出 STL 做 3D 打印教学模型或者再接一步 VR/AR 设备做沉浸式解剖学习。建议第一次做先从一个样例数据把全链路跑通再考虑扩大数据规模和结构数量。整套流程跑下来你手里就不只是三张解剖图而是一个可以随时扩展的人体数字化工程。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进