ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

OCR+视觉大模型+四点定位:医疗单据脱敏的工程实践

OCR+视觉大模型+四点定位:医疗单据脱敏的工程实践 1. 为什么医疗单据脱敏不能靠现成的“打码滤镜”直接搞定1.1 一个看似简单实际翻车的场景拍照单据我接手过一个医疗单据批量归档项目甲方最初的需求很轻描淡写“把单据上的姓名、身份证号、手机号、家庭住址打码就行。”我一开始也觉得这活儿简单找个现成的OCR抽文字正则匹配一下身份证和手机号再用矩形覆盖掉半天就能交付。真正拿到样张之后才发现事情完全不是这样。这些医疗单据不是干净的扫描件而是工作人员用手机拍的有的单据被折过一道有的拍摄角度偏了30度有的局部还有阴影和反光。姓名在表格里地址跨了三行病历描述里藏着“患者自述”“既往史”这类不能公开的叙述。如果用固定位置打码稍微旋转一点就对不上如果用矩形框粗暴覆盖纸上的表格线、印章、二维码都会被连带涂掉片子根本没法看。那时候我才意识到医疗单据脱敏本质上不是一个图像处理问题而是一个“先理解版面再理解语义最后精确破坏敏感区域”的复合问题。标题里那句“OCR Vision LLM 四点定位打码”并不是三个模块的简单叠加而是分别解决“文字在哪”“哪些算隐私”“怎样打码不伤版面”这三件事。1.2 通用脱敏方案的三大缺陷漏检、错检、破坏版面市面上现成的脱敏工具我也试用过几轮对干净扫描件还行一旦落到复杂拍摄场景问题就暴露出来了。第一个缺陷是漏检。手机拍照产生的透视变形会让OCR的检测框发生偏移。角度稍微大一点文字就容易粘连在一起身份证号被识别成乱码手机号漏掉中间几位。漏检意味着敏感信息原样留在图片里这在医疗场景是绝对不能接受的。第二个缺陷是错检。很多工具为了求稳把所有数字都当成潜在手机号把“2024-03-15”“门诊号0382910”也一并打码。结果病人姓名没遮住反而把就诊日期和单据编号全涂了归档审核时直接被打回。第三个缺陷是破坏版面。矩形框打码在水平文档上还能用但医疗单据上的敏感字段往往紧挨着表格线、印章或者手写签名。一旦矩形框稍有偏差就会涂掉无关内容或者留下半个字在外面。更麻烦的是弯折单据上的文字区域是变形的矩形框根本包不住一个倾斜的多边形文字块。所以后来我把方案定成了“四点定位打码”——每个敏感字段用任意四边形框住而不是用轴对齐矩形。1.3 拆解敏感信息类型字段级语义比正则更可靠脱敏的第一步其实是搞清楚一张医疗单据上到底哪些内容需要处理。常见的敏感类型包括姓名、性别、年龄、出生日期等基本身份字段身份证号、医保卡号、就诊卡号、住院号等证件编号手机号、家庭住址、联系人信息既往病史、过敏史、主诉、诊断描述等病历叙述检查报告中的“患者姓名”“送检医生”“报告医生”等关联字段。前两类靠正则还能勉强覆盖但“既往病史”“主诉”这种自由文本正则没有办法判断哪个词该打码。我把这些字段梳理了一遍之后结论很明确必须引入一个有语义理解能力的视觉模型让它直接看图说话判断某个OCR文本框对应的内容属于哪一类敏感字段。这就是Vision LLM在这个项目里的核心位置。2. 方案选型思路OCR、视觉大模型和四点定位各自承担什么2.1 OCR选型与适用边界要的是文本框不是纯文本项目的第一步是定位文字位置。这里有个很容易走偏的点很多人把OCR理解成“把图变成文字”但在这个项目里OCR输出的文字内容只是辅助信息真正重要的是每个文字块对应的坐标。我测试过几种主流的开源OCR框架最后留下了两个候选一种是学术型检测识别分离的框架检测模型输出文本框识别模型再输出文字另一种是端到端模型直接输出带坐标的文字结果。在复杂拍摄场景下检测识别分离的框架更容易调优因为多数漏检问题出在检测环节我可以单独强化检测模型而不影响识别逻辑。选型时有几个关键参数需要关注文本框格式是否支持任意四边形四点坐标还是只有水平矩形。支持四点坐标的模型能更好地贴合倾斜、旋转的文字块。多方向文本支持医疗单据上既有横向打印文字也有竖向手写备注甚至印章里的文字是旋转排列的。选型时要看模型训练数据里是否包含多方向文本。推理速度单片单据的目标延迟最好控制在2秒以内否则批量归档跑不动。2.2 视觉大模型负责理解语义哪些文字是隐私OCR只能回答“这里有文字”不能回答“这句话是不是隐私”。医疗单据里“张某某”可能是患者名也可能是医生名“138XXXX1234”可能是患者手机号也可能是医院咨询电话。这些都需要语义判断。Vision LLM在我的方案里承担三件事对每个OCR文本框做字段类型分类输出“姓名/证件号/联系电话/地址/病史描述/非敏感”等标签综合上下文判断遮挡范围比如遇到“联系人李某某关系夫妻”这种一行里出现两个字段的情况要分别识别对OCR漏检做二次补充视觉模型可以同时看整张图和所有文本框如果发现某段文字明显像身份证号但OCR没框出来就触发重新检测。实际使用中Vision LLM的输入不是整张大图直接输入而是把OCR裁剪出的文本块图像和对应的文字内容一起送进去。这样能减少干扰也能让模型把“图像里的文字样式”和“文字语义”结合起来判断。比如手写的“主诉咳嗽三天”会被判定为敏感病历信息而表格里的“项目名称血常规”则不会。2.3 四点定位打码解决多边形旋转不规则区域避免矩形误伤为什么不用矩形框我用一个具体例子说明。一张门诊收费票据上“姓名张某”这个字段在表格里紧挨着竖线如果按OCR框的水平外接矩形打码矩形会向右扩展把旁边的金额数字也盖住。但如果用OCR检测出的四边坐标直接形成任意四边形就能只盖住文字本身不碰到表格线。四点定位的核心是维护好“四边形的四个角点”到“实际图像像素区域”的映射关系。在预处理阶段图片可能被矫正、缩放所以所有坐标都必须在一个统一的坐标系里跟踪。我的做法是OCR输出的坐标先按原始图片尺寸归一化预处理环节的所有变换旋转、透视矫正、缩放都记录变换矩阵打码前用变换矩阵逆运算把OCR坐标映射回最终输出图的坐标空间对四边形做整体外扩避免只盖住文字主体而漏掉边缘笔画。这套流程跑通之后即便单据被弯折过只要OCR能检测出文字块四点坐标就能贴合文字的实际形状打码边缘不会出现“有些笔画盖住了有些笔画还露在外面”的情况。3. 复杂拍摄场景的适配弯曲、旋转、透视畸变的预处理链路3.1 为什么模型直接跑会掉点拍摄角度的透视变形刚开始我用原始图片直接跑OCR结果让我很意外印刷体的识别准确率尚可但文本框的位置经常整体向右下方偏移导致后边的打码框跟不上。仔细分析了原因手机拍摄单据时镜头平面和单据平面不平行产生了透视变形。文字在图像上不再是一行水平排列而是近大远小、行线收敛于远处某一点。透视变形最直接的影响是OCR检测框的“倾斜角”变大。模型能检测出斜框但置信度下降偶尔会把一行文字断成两块或者把相邻两行粘连在一起。如果不在预处理阶段做矫正后面每一步的误差都会累积。3.2 弯曲矫正基于文本行几何分布的矫正思路单据被折叠或弯折时文字行会变成曲线。比如一张横向折过的化验单折痕附近的文字会被压缩下面的行会轻微扭曲。对这种弯曲我尝试过传统的网格矫正但效果一般因为单据上的表格线会干扰网格生成。后来换了一个思路先跑一遍OCR拿到所有文本行的四点坐标然后按文本行中心点拟合弯曲曲线再根据曲线把图像展开。这个方法不需要额外的矫正模型纯粹基于OCR结果做几何变换实现起来更稳。具体流程是收集所有文本行的中心点坐标按行分组对每一行拟合二次曲线以曲线为基准估计行之间的间距变化用分段仿射变换把弯曲区域映射到水平规则网格。这套方法对轻度到中度弯折效果很好对严重弯折比如单据被揉成一团再展开效果有限那种情况只能提示用户重新拍摄。3.3 旋转和透视处理的统一坐标映射旋转和透视可以统一用矩阵运算处理。实际操作中我维护了一个全局变换栈每次对图像做旋转、缩放或透视矫正都会把一个对应的变换矩阵压入栈中。最终输出打码结果时把OCR原始坐标经过所有矩阵变换得到当前图像坐标。这里最容易踩的坑是图像尺寸变化。有些图像处理库在执行透视变换后会自动扩大画布以保证内容不丢失导致图片尺寸改变。如果代码里仍然用原始坐标打码所有框都会偏移。我的解决办法是每次变换后都重新获取图像的新尺寸并把变换矩阵更新为“从原始图像坐标到当前图像坐标”的完整映射。3.4 预处理链路的实际效果对比预处理链路处理好之后OCR的F1值在测试集上提升非常明显。我用500张手机拍摄的医疗单据做对比未预处理时文本检测的召回率大概只有82%加完旋转和透视矫正后召回率提升到93%再加上弯曲矫正提升到96%。最关键的是文本框的稳定性好了很多不再出现同一张单据跑两次结果不一样的情况。值得注意的是预处理不能做得太激进。过度矫正会把单据拉成奇怪的形状反而让OCR更困惑。我最后采用的策略是只有当检测到文本行倾斜角度中位数超过15度或者文本行中心点拟合曲线的残差超过阈值时才触发矫正。在大部分轻度倾斜的场景下直接让OCR自适应反而更快。4. 核心实现从检测到脱敏的完整流程4.1 第一层OCR文本框提取与置信度过滤整个脱敏流水线分三层。第一层是OCR文本检测与识别。代码结构大致如下def extract_text_boxes(image): # image: 经过预处理的BGR图像 result ocr_detector.predict(image) boxes [] for line in result: box line[box] # 四个点的坐标 [[x1,y1],...] text line[text] score line[score] if score 0.45: continue boxes.append({ box: box, text: text, score: score }) return boxes置信度过滤不能一刀切。对于打印体字段阈值调到0.5都没问题但对于手写体、印章、带背景纹理的文字阈值如果设太高会把该检测的字段漏掉。我最终的策略是先把所有大于0.3的框全部留下来交给视觉大模型做过滤视觉模型认为某个区域重要但置信度低时会触发二次检测。4.2 第二层视觉大模型语义分类内部/公开字段拿到所有文本框后把每块文本的裁剪图像和OCR文字组合起来送给视觉大模型。这里的Prompt设计很关键。我早期的做法是让模型直接输出JSON标签但模型容易把“既往史”里的“史”误判成“时间”。后来我把问题改成更具体的分类如果是身份字段返回包含“姓名”“证件号”“手机号”等具体类型如果是医疗叙述字段返回“主诉/现病史/既往史”等子类型如果既不属于身份也不属于医疗叙述返回“公开”。Prompt长这样你正在处理一张医疗单据。下面每个文本块可能包含敏感信息。 请对每个文本块进行分类类别包括: 姓名、性别、年龄、证件号码、联系电话、家庭住址、医保信息、主诉、现病史、既往史、诊断结论、检查报告、非敏感。 输出JSON数组包含每个文本块的id和category。 注意: 根据文本内容和上下文综合判断不要仅凭单一关键词。为了让模型不“乱联想”我会把整张单据的OCR文本按阅读顺序拼成上下文让模型参考上下文来分类。比如“患者张某某联系电话138...”两个字段在一个文本块里单看前半句可能是姓名后半句又是电话模型会切开成两条。这一步是纯规则很难做到的。4.3 第三层四点坐标定位与膨胀掩码分类完成后敏感字段进入打码模块。打码不是直接把四个点围成的区域涂黑而是要把坐标应用到目标图像上并做膨胀和边界羽化。膨胀的原因很简单OCR检测框通常紧贴文字主体但有些字符的笔画会略微超出检测框比如“张”的捺、“电”的竖弯钩。如果不膨胀打码后边缘会留下明显的笔画残影。我的默认膨胀参数是文字高度的10%最小膨胀6像素最大膨胀20像素。生成掩码的代码片段import numpy as np import cv2 def mask_quad(image, quad_points, expand_ratio0.1): pts np.array(quad_points, dtypenp.float32) center pts.mean(axis0) expanded_pts center (pts - center) * (1 expand_ratio) mask np.zeros(image.shape[:2], dtypenp.uint8) cv2.fillPoly(mask, [expanded_pts.astype(np.int32)], 255) return mask注意这句center (pts - center) * (1 expand_ratio)它是以四边形中心为基准向外扩展相比单纯在每个坐标加减像素值能更好地保持四边形的形状。打码最终采用“掩码马赛克平均”的组合。直接涂纯色块在票据上是明显的干预痕迹归档时没那么好看用马赛克则保留了“这里被遮住了”的信息又不至于太突兀。实测下来马赛克颗粒取8像素时既能掩盖信息又不太影响版面阅读。4.4 脱敏后图像质量控制Mask和模糊的选择打码后的图像质量直接影响项目能不能验收。我试过三种方式纯色块覆盖像素替换为灰白色简单粗暴但会在彩色印章旁边显得非常刺眼高斯模糊对四边形区域做高斯模糊边缘自然但模糊半径不够时文字的轮廓还会隐约看到马赛克化将区域分割成小方块每个方块填平均色既不可读又保留了一定的“不确定性”。最终我采用“高斯模糊马赛克混合”先用较大半径的高斯模糊把文字信息彻底抹掉再做一层轻量马赛克。这样既没有明显色块边界也不会被还原出文字轮廓。4.5 开源项目目录与模块划分项目开源之后我把代码划分成五个模块方便不同需求的用户只取其中一部分preprocess/ # 图像预处理矫正、去噪、透视变换 ocr_detector/ # 文本检测与识别封装 semantic_filter/ # 视觉大模型语义分类 masking/ # 四点定位打码与图像后处理 pipeline/ # 端到端串联这样划分的目的是有的人只需要OCR模块有的人只想用语义分类模块有的人干脆自己写OCR只借用四点打码的代码。模块之间通过标准的JSON数据结构传递不强依赖某一个具体OCR框架。5. 实测效果与被忽略的坑5.1 测试集构建模拟弯折、旋转、拍摄模糊场景为了验证方案我专门构建了一个测试集把1000张医疗单据脱敏过的测试数据分别做旋转、弯折、透视变换、加噪声、模拟反光等处理生成2000张测试图。测试集一定要覆盖真实环境否则指标再高也没用。我的测试集分几类正常平铺拍摄基线15度到30度的透视倾斜模拟手持拍摄横向对折后展开模拟单据折痕局部反光与阴影模拟灯光不均匀低分辨率手机图模拟老旧设备。5.2 指标统计召回率、误杀率、版面保持度评估指标不能只看OCR准确率。脱敏项目更关心的是敏感字段召回率所有敏感字段中有多少被成功打码误杀率非敏感字段中有多少被错误打码版面保持度打码后非敏感区域的像素变化比例。我的实测结果大致是敏感字段召回率97.8%误杀率1.9%版面保持度约96%。这个结果是在包含倾斜和弯折的测试集上跑出来的。召回率没有到99%主要问题出在极端反光和手写小字上后面会细说。5.3 几个经典翻车案例低对比度、反光、手写体第一个翻车案例是低对比度单据。有些用热敏纸打印的单据过一段时间字迹变淡OCR检测置信度极低。视觉大模型虽然能看到模糊的痕迹但无法准确判断文字边界导致打码框偏大把旁边表格里的“年月日”也包进去。针对这类情况我的对策是增加一个图像增强分支检测到图像整体对比度偏低时先做自适应直方图均衡化再送OCR。第二个是反光区域。反光处会出现高光斑块文字被亮斑切断OCR识别出来是“1 3 8 ****”中间全是星号。视觉大模型根据上下文推断这是手机号但四个点坐标已经不准确了。这种情况下我做了“区域重新检测”以反光影响区域为中心截取局部图用全局阈值分割找出文字笔画再用笔画的最小外接多边形替换原来的四点框。第三个是手写体。医疗单据上的医生签名和手写备注很难识别。打码这类区域时我不依赖OCR识别的文字内容而是让视觉模型判断“这块区域包含手写且属于敏感类别”只要判断是敏感类别就用手写区域的连通域掩码来做打码效果比OCR框好很多。5.4 视觉大模型误判地址拆行识别为多个字段Vision LLM最大的问题不是不会分类而是容易把同一个字段拆成多个不完整的字段。比如“北京市朝阳区某某街道某小区3栋502室”在单据上可能分成三行显示模型会把每行都识别成独立地址打码时三块之间留下缝隙或者把相邻的“联系人”字段也划进去。我的解决办法是把OCR的布局信息注入Prompt把属于同一行、同一列的文本块合并成“段”让模型在段落级别上做分类而不是逐个文本块分类。段落合并的依据是文本块中心点垂直方向上的重叠程度。如果两个文本块在垂直方向有较多重叠且水平间距不大就合并成同一个逻辑段。合并之后地址、主诉这类跨行文本的分类准确率明显提升。6. 工程落地建议性能优化与离线部署6.1 各环节的耗时占比与优化空间我在CPU机器上先跑了一版单张单据的总耗时接近15秒其中OCR检测占5秒识别占4秒视觉大模型分类占5秒图像处理占1秒。这个速度完全没法用于是开始逐环节优化。优化后单张单据在CPU上的耗时压到4秒左右GPU上能到1.5秒。主要手段包括把OCR检测和识别拆成并行、视觉大模型用量化后的轻量版本、图像尺寸动态缩放。各环节耗时参考环节优化前耗时优化后耗时主要优化手段OCR检测5.0s1.2s限制检测图像最长边设置信度阈值OCR识别4.0s1.0s只识别检测出的文本块裁剪小图视觉大模型分类5.0s1.5s量化减少输入Token数图像预处理与打码1.0s0.3s使用轻量图像处理库避免重复编解码6.2 OCR结果缓存与视觉模型裁剪医疗单据有一个特点同一家医院、同一类单据的版式相对固定。所以我在工程上加了OCR结果缓存。缓存键是图像的感知哈希和文件大小组合如果同一张图片重复提交直接复用之前的检测结果和脱敏掩码省去全部模型计算。视觉模型部分我用的是开源视觉语言模型权重的蒸馏版本从原来的单精度浮点模型量化到8位整数显存占用降低了一半多分类准确率只下降不到1%。输入图片也裁剪成短边512像素对于大多数单据文字识别来说这个分辨率足够。6.3 批量任务、异步队列、GPU/CPU混合调度归档场景是典型的海量离线任务必须做异步队列。我用了一个简单的生产者消费者模型def process_queue(image_queue, result_queue, workers4): with ThreadPoolExecutor(max_workersworkers) as executor: futures [executor.submit(worker, image_queue, result_queue) for _ in range(workers)] for f in futures: f.result()这里要注意OCR和视觉模型最好分开跑。GPU资源紧张时OCR在GPU上视觉模型可以退到CPU如果两个模型抢同一个GPU显存容易出现内存不足。我的调度策略是前处理和后处理用多线程并行模型推理部分如果机器有多个GPU按照显存余量分配模型实例如果只有一个GPU把视觉模型放进批处理模式每批4到8张图吞吐量比单张推理提升2倍以上。6.4 私有化部署与隐私合规要点脱敏工具本身处理的就是敏感数据部署时必须考虑数据不能出内网。我的开源版本只提供离线推理方案所有模型权重和推理脚本都放在本地运行。服务端不保存原始图像处理完成后只保留脱敏后的结果图并且处理日志里不写入姓名、证件号这类明文。另外单据测试数据在项目里全部是公开的模拟数据。真正涉及真实患者信息时必须先做好数据授权。这个原则是底线技术方案再完善也不能越过去。7. 开源项目可以怎么上手用7.1 最小运行步骤拿到开源仓库之后最快跑通的方式是安装依赖库下载OCR和视觉模型权重准备一张测试单据图片运行命令行工具python run_pipeline.py --input test.jpg --output result.jpg默认参数会输出两张图一张带检测框的标注图一张打码后的结果图。如果只想用某个模块也可以单独调用。比如只做OCR检测python run_pipeline.py --input test.jpg --output result.json --module ocr7.2 如何调整敏感字段范围不同机构的脱敏要求不一样。有的只要遮姓名和身份证号有的连检查项目里的特定词都要处理。开源代码里把所有分类规则统一放在一个配置文件里可以按需调整{ sensitive_categories: [姓名, 证件号码, 联系电话, 家庭住址, 主诉, 现病史], mask_method: mosaic, expand_ratio: 0.1 }调整后不需要重新训练模型视觉大模型会按照新的分类要求输出标签打码逻辑跟着标签走。要注意的是如果增加一个原本不在Prompt里的新类别需要同步修改Prompt模板否则模型不知道新类别是什么意思。7.3 数据回流与模型微调思路虽然默认权重在大多数单据上表现不错但每个机构的单据版式都有自己的“土特产”某些医院喜欢在单据角落盖一个大红章有些社区中心的表格线又细又密。如果希望准确率更高需要做模型微调。我的建议是优先微调OCR检测模型采集50到100张该机构的脱敏后单据标注出文字框然后用检测模型继续训练少量轮次。视觉大模型除非分类错误特别集中否则不建议直接微调成本高且容易过拟合。先用规则修正和Prompt调整来兜底大部分问题都能解决。一点个人实测体会项目做完之后我最深的感受是医疗单据脱敏这种需求表面看是OCR加图像处理实际做下来会倒逼你把“识别、理解、图像变换”三块能力全部打通。尤其四点定位打码看起来只是坐标映射但一旦和弯曲矫正、透视变换、OCR框不确定性叠加在一起细节就会成倍增加。建议后来者先不要追求完美把流水线跑通再用真实数据慢慢磨边界情况。这套思路不只是医疗单据能用合同、发票、档案、表单脱敏都可以参照只是字段分类需要重新调整。我把这个项目整理成开源仓库之后也一直在持续补充新的测试场景希望能让更多人在处理这类问题时少走点弯路。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表