AI病理分析:结构化证据提取链路怎么搭,才能真正进入科研流程
病理数字切片进入 AI 分析后,工程难点往往不在“跑一次模型”,而在切片切块、模型推理、区域聚合、证据沉淀和人工复核能否串成稳定链路。本文只讨论技术架构和工程流程示例,不提供诊断、治疗、分诊或用药建议;文中阈值和规则均为可配置示例,真实项目应由医疗专业人员和机构规范确认。
背景:科研流程需要的是证据链,不只是预测值
在病理分析项目里,单张 WSI 文件可能达到 GB 级别。后端服务如果直接把整张切片送进模型,通常会遇到显存不足、推理耗时不可控、结果无法定位、复核人员看不到证据来源等问题。
更适合科研工具的做法,是把 WSI 拆成可追踪的小块,每个 patch 产生模型输出,再把 patch 级结果聚合为区域级、切片级证据。最终保存的不应只有label=positive,而应包含坐标、倍率、模型版本、置信度、聚合规则、复核状态和导出记录。
技术目标与约束
这类后端服务建议先明确 5 个目标:
- WSI 原文件进入 object storage,数据库只保存元数据和索引。
- OpenSlide 负责按倍率和坐标读取 patch,避免一次性加载整图。
- PyTorch 模型只处理标准化 patch,并记录模型版本。
- PostgreSQL 保存任务、patch、region、evidence、review 等结构化数据。
- FastAPI 暴露任务提交、状态查询、证据导出、复核回写接口。
同时要避免把模型输出直接写死为科研结论。系统输出的是“可复核证据”,不是自动结论。任何风险分层、升级规则或阈值都应实现为配置项,并在项目落地时由机构规范确认。
总体架构:从 WSI 到 Evidence JSON
下面是一个后端服务的简化链路:
核心思想是让每一步都可重跑、可追踪、可审计。比如模型升级后,只需要基于同一批 patch 重新推理,聚合规则变更后,只需要重算 region 和 evidence,不必重复上传和解析 WSI。
数据模型:先把证据字段设计清楚
我通常会把数据拆成 4 层:
slides:切片级元数据,如文件地址、扫描倍率、尺寸、hash。patches:切块索引,如 x、y、level、width、height、tissue_ratio。patch_predictions:模型输出,如 class、score、model_version、created_at。evidences:区域聚合结果,如 bbox、summary、rule_version、review_status。
一个证据 JSON 可以长这样:
{"slide_id":"S20260524001","region_id":"R-0007","bbox":{"x":18432,"y":9216,"w":4096,"h":3072},"model_version":"path-ai-v0.3.2","rule_version":"agg-rule-2026-05-demo","summary":{"positive_patch_count":18,"mean_score":0.82,"max_score":0.94},"example_rule":"mean_score >= configurable_threshold","review_status":"pending"}这里的configurable_threshold只是示例规则占位,不代表任何通用医学标准。工程上要保留规则版本,否则后续科研统计时很难解释不同批次结果的差异。
实现步骤:FastAPI 提交任务,后台执行切块和推理
下面代码展示一个最小可运行思路:上传任务只登记 WSI 地址,实际切块和推理由后台任务执行。生产环境建议把BackgroundTasks替换为 Celery、RQ 或 Kafka 消费者。
fromfastapiimportFastAPI,BackgroundTasksfrompydanticimportBaseModelfromtypingimportList,Dictimportuuidimportopenslideimporttorch app=FastAPI(title="Pathology Evidence Pipeline Demo")classAnalyzeRequest(BaseModel):slide_uri:strslide_id:strlevel:int=0patch_size:int=512stride:int=512model_version:str="path-ai-v0.3.2"classEvidence(BaseModel):slide_id:strregion_id:strbbox:Dict[str,int]model_version:strsummary:Dict[str,float]review_status:str="pending"defload_model(model_version:str):model=torch.nn.Sequential(torch.nn.Flatten(),torch.nn.Linear(512*512*3,2))model.eval()returnmodeldefinfer_patch(model,patch_image):# 示例:真实项目需加入颜色标准化、组织区域过滤和模型预处理score=0.8return{"class_name":"example_positive","score":score}defaggregate_predictions(slide_id:str,predictions:List[Dict],model_version:str):selected=[pforpinpredictionsifp["score"]>=0.75]ifnotselected:return[]xs=[p["x"]forpinselected]ys=[p["y"]forpinselected]scores=[p["score"]forpinselected]evidence=Evidence(slide_id=slide_id,region_id=f"R-{uuid.uuid4().hex[:8]}",bbox={"x":min(xs),"y":min(ys),"w":max(xs)-min(xs)+512,"h":max(ys)-min(ys)+512},model_version=model_version,summary={"positive_patch_count":float(len(selected)),"mean_score":float(sum(scores)/len(scores)),"max_score":float(max(scores))})return[evidence.model_dump()]defrun_pipeline(req:AnalyzeRequest):slide=openslide.OpenSlide(req.slide_uri)width,height=slide.level_dimensions[req.level]model=load_model(req.model_version)predictions=[]foryinrange(0,height-req.patch_size,req.stride):forxinrange(0,width-req.patch_size,req.stride):patch=slide.read_region((x,y),req.level,(req.patch_size,req.patch_size)).convert("RGB")result=infer_patch(model,patch)predictions.append({"x":x,"y":y,"score":result["score"],"class_name":result["class_name"]})evidences=aggregate_predictions(req.slide_id,predictions,req.model_version)# 示例:生产环境应写入 PostgreSQL,并记录任务状态、耗时和异常print({"slide_id":req.slide_id,"evidence_count":len(evidences)})@app.post("/analysis/jobs")defcreate_job(req:AnalyzeRequest,bg:BackgroundTasks):job_id=uuid.uuid4().hexbg.add_task(run_pipeline,req)return{"job_id":job_id,"status":"submitted"}这段代码没有追求模型真实性,而是强调链路边界:任务提交、OpenSlide 切块、patch 推理、区域聚合、证据输出。实际项目中应把模型加载、预处理、数据库写入、对象存储读取拆成独立模块。
PostgreSQL 与对象存储怎么分工
WSI 原文件、缩略图、patch 缓存适合放对象存储。PostgreSQL 更适合保存可查询的结构化索引,例如任务状态、切块坐标、推理结果、证据区域和复核记录。
一个常见错误是把 patch 图像二进制直接塞进数据库,短期方便,后期备份、迁移、查询都会变重。更稳妥的方式是数据库保存object_key、坐标和 hash,需要展示时再从对象存储按需读取。
证据表建议至少包含这些字段:slide_id、region_id、bbox、model_version、rule_version、summary_json、review_status、reviewer_id、review_comment、updated_at。科研流程中,复核回写和模型输出同等重要。
踩坑记录:工程化时最容易断的地方
第一,坐标系混乱。OpenSlide 的 level 坐标和原始 level 0 坐标要统一记录,前端叠加标注时尤其容易偏移。建议 evidence 一律保存 level 0 坐标,展示端自行换算。
第二,patch 数量失控。全量滑窗会让任务时间膨胀,通常要先做组织区域过滤,再进入模型推理。过滤规则可以从简单的背景比例开始,但阈值必须作为配置项保存。
第三,模型版本和规则版本缺失。科研分析需要复现,不记录版本就无法解释结果差异。每次导出 evidence 时,应带上模型权重版本、推理参数和聚合规则版本。
第四,复核结果无法回写。只导出 CSV 而没有 review API,会导致人工修订散落在表格里。建议提供PATCH /evidences/{id}/review,把复核状态、备注和操作者写回系统。
扩展方向:让证据输出进入科研闭环
当基础链路稳定后,可以继续做三类扩展:
- 增加任务队列和 GPU worker,实现多切片并行推理。
- 增加 evidence 导出格式,如 JSONL、CSV 和项目内部标注格式。
- 增加复核一致性统计,用于评估模型版本和规则版本的变化影响。
需要强调的是,系统的价值不止在于识别结果,而在于把“结果来自哪里、由哪个模型产生、经过什么规则聚合、谁复核过”记录下来。只有证据输出和复核回写都闭合,AI 病理分析才更容易进入可追踪、可复现的科研流程。
本文文献检索、文献挖掘以及文献翻译采用的是【超能文献| AI文献检索|AI文档翻译】。
