家庭药片识别实战:从数据集构建到移动端部署
去年秋天整理自家药箱我对着半抽屉散装药片发了好一会儿呆。老人把几板铝塑药都剪开了锡箔包装扔的扔、混的混剩下的药片有的白有的黄有的压着字母有的光秃秃手机扫码找不到原包装说明书更是早没了踪影。我决定做个叫 Smart Pill Identifier 的小项目定位就是 Home Medication Recognition手机摄像头对准一粒药识别它是什么药、对应哪个产品、有哪些风险提示。这个项目做了大半年中间踩了很多坑也把整套识别链路从数据集、模型、端侧部署到产品交互重新捋了一遍。这篇文章不写Demo式教程而是完整记录我在方案取舍、数据工程和落地测试里的真实过程给同样想碰移动端图像识别的人做个参考。1. 家庭药箱场景下的真实痛点与选型判断做任何视觉项目我都会先问一句为什么这个问题非要用图像解决家庭用药识别也一样。如果包装还在扫码是最简单可靠的方案根本不需要模型。真正麻烦的是药品脱离包装后的形态而这个形态在家庭里太常见了。1.1 为什么扫码方案会在家庭场景失灵严格说扫码没有失灵只是适用面太窄。药房、医院里每盒药都有唯一码收银结算、药品追溯都靠它。但家庭场景里药品从买回来那天就开始“变形”铝塑泡罩板被剪成小块方便随身带条形码区域往往在边缘一剪就没了瓶装药开封后说明书扔在一边药片倒进分药盒后只留下一堆裸片慢性病患者还习惯把一周的药混在一个七格盒里时间一长连药名都忘了。当手里只剩一粒裸片时没有任何码可扫。也有人会说用药盒上的文字做OCR行不行药盒还在的情况确实可以但药盒也面临磨损、反光、字体小、中英文混排的问题而且它同样依赖“原包装存在”这个前提。RFID芯片在家庭场景更是无从谈起药厂不会在单个药板上贴芯片家里也不会有读卡设备。所以扫码、OCR、RFID这些方案本质上都是围着“包装”打转一旦包装消失了它们就全都不成立。1.2 图像识别是唯一能覆盖“裸药片”的输入方式我把能想到的方案拉了一张对比表从识别基础、家庭裸药片场景的可用性、关键限制三个维度看方案识别基础裸药片场景可用性关键限制条形码/追溯码包装上的码低包装必须完整码区域常被剪掉药盒文字OCR盒体印刷文字中依赖原包装受光照和磨损影响RFID/NFC标签芯片极低家用药品基本没有此类标签药片视觉识别形状、颜色、刻痕、表面信息高需要足够的数据与外观特征支撑结论很清楚想覆盖“散装裸药片”这个高频场景视觉识别是绕不开的路径。它不需要任何额外标签只要摄像头能拍到药片就能尝试判断。这也是 Smart Pill Identifier 的第一条设计原则不依赖包装直接面向药品本身。确定方向后我原本以为最难的是模型后来发现最难的是数据这也是下一个章节要详细展开的内容。2. 药片识别数据集搭建比想象中脏十倍模型训练经常讲数据集决定上限这句话在药片识别上体现得淋漓尽致。药片的外观不是均匀的工业平面光照、角度、老化、切割都会改变它数据集如果覆盖不到模型一定在真实场景翻车。2.1 先拆解“药的三种形态”裸片、胶囊、刻痕片药片在自然语言里都叫“一片药”但视觉上差别很大。我把训练对象分成三类压制裸片比如圆形、椭圆形的布洛芬片、对乙酰氨基酚片表面可能有刻痕或字母压印胶囊类由两个半壳组成颜色可能分两段比如阿莫西林胶囊、头孢类包衣片和缓释片表面光滑有包衣层反光严重。这三类在不同光照下表现差异极大。裸片表面粗糙漫反射为主颜色相对稳定包衣片和软胶囊则容易出现镜面反光反光会直接压掉一部分颜色信息。我后来做数据采集时专门给这三类设计了不同的拍摄要求裸片要保证主色占比够大胶囊要拍到两段颜色的分界刻痕片必须把刻痕拍清楚不能因为压印太浅就忽略。颜色处理上我建议把RGB转成HSV之后再统计色值。同一粒药在日光灯下和LED灯下RGB差异很大而HSV中的色相H相对稳定一些更适合作为标注基础。2.2 标注标准定了之后遇到的分歧药片数据集看起来简单拍一张图标一个类别。但真正动手标几百个品种后分歧全出来了。第一个分歧就是颜色。同一个药片在5600K色温下是淡黄在低色温台灯下可能变成橘黄。我的处理办法是打印一张包含常见药片色的锚定色卡标注时先把药片放到色卡旁边拍一张所有颜色标签都参照色卡来定。一开始觉得麻烦后面发现这套方法显著降低了不同标注者之间的偏差。第二个分歧在刻痕。有的药片十字刻痕十分明显有的只有一条浅线。标注标准里必须写清楚“浅线也算有刻痕”并额外标注刻痕方向否则模型会把“有痕”和“无痕”混在一起。这里还有个大坑有些药片表面为了防滑压了细花纹不是功能性刻痕数据里统一算“无刻痕”避免给模型增加噪声。数据来源方面国外开源药片数据大多以美国市场药品为主和中国家庭常见药的重合度很低。我最终的做法是公开数据集仅作预训练参考主体数据自己采集。实际流程是列一个常见药清单去线下药店买正品回来后在统一灯箱和不同生活场景各拍一轮每个品种最终攒到150到300张。300类大约需要6万到9万张听起来多但拍摄节奏熟练后一天拍40个品种是可以做到的。2.3 数据增强与过拟合陷阱有了数据下一步是增强。增强不能瞎加。圆形压制片本身具有旋转不变性对它们做任意角度旋转模型学到的是真实存在的方位没有坏处但对有刻痕的片剂旋转增强会把“一字刻痕”的方向变成噪声所以我会把增强分成普通类与刻痕类两套管线。真正提升真实场景效果的增强我实测下来是这几种亮度扰动、高斯模糊模拟手抖、透视变换模拟手机倾斜、随机裁剪模拟药片不在画面中心、椒盐噪声模拟老手机Sensor问题。还有一个容易忽略的是反光增强。包衣片和软胶囊在真实环境里最容易出现高光斑块训练时给图像叠加椭圆形高光Mask测试时识别率能涨好几个点。过拟合的信号也很典型训练集99%、测试集95%一到真实客厅灯下就只剩80%。这说明模型记住的是拍摄环境不是药片本身。我后来直接在训练集里加入大量复杂背景样本而不是只做干净白底才把真实环境的准确率拉回可用水平。另外测试集必须单独留出每个品种至少15张用家人随手拍的手机照片不能用训练同源照片测否则分数全是自欺欺人。3. 两段式识别管线检测加检索而不是单模型硬怼药片识别最直觉的做法是端到端分类模型拍一张图输出药品名。我在项目一开始也是这么试的很快发现三个问题。第一画面里除了药片还有桌面、手掌、药盒背景分类模型会被无关区域带偏第二家庭药品类别不是封闭的这月吃阿莫西林下月换成阿奇霉素每换一次药就重训一次模型维护成本太高第三模型直接输出“类别标签”排查错误时很难定位是哪里学歪了。所以我把管线改成了两段式先检测再检索。3.1 第一段目标检测框出药片第一段用目标检测模型把画面里的药片位置找出来。我选的是YOLOv8的n版本输入640x640量化后模型体积不到6MB在中端手机上跑CPU也能有几十毫秒的推理时间。检测目标只设一个类别pill。刻意不做多类别检测是因为把几百种药都当检测类别会让检测头同时学“长什么样”和“是哪一药”两件事反而更容易混淆检测头只负责框出“像药片的目标”识别交给第二段。整个识别链路可以用一段伪代码描述输入相机帧 frame 输出药品候选列表 1. 检测模型推理 frame得到 pill_bbox 2. 裁剪 pill_bbox内缩 5%缩放至 224x224 3. 特征模型提取 embedding encode(crop) 4. 遍历本地特征库计算 cosine_similarity(embedding, db_feats) 5. 按相似度排序依据双阈值输出唯一/候选/拒绝检测的输出是边界框。拿到框后把框内区域裁剪出来缩放到224x224作为第二段输入。这一步看似简单实际对识别率影响很直接框裁剪得准胶囊两端颜色比例才完整框松了背景混进去就会带偏特征。我给检测模型加了一个后处理将边界框向内收缩5%能把背景干扰降一截代价是可能丢掉刻痕边缘。是否采用需要按数据实测不同药品的敏感度不一样。3.2 第二段分类还是度量学习第二段的选择题是做Softmax分类还是做特征检索两者都可以识别药片但适用场景完全不同。分类方案的优点是实现简单模型最后一层直接输出类别概率缺点也很明显类别列表是训练时写死的新增一个药品就要收集数据、重新训练、重新发版。更麻烦的是模型对没见过的药片会强行分到最接近的类里这种错误在药品场景里不可接受。我最终用的是度量学习方案。主干网络取EfficientNet-Lite0最后接一个128维的Embedding层训练时用ArcFace损失函数让同品种特征靠近、不同品种特征拉开。推理时分两步先给库里每个药品SKU提取一次特征存成文件手机启动时加载新识别时把当前药片的Embedding和库里特征做余弦相似度排序相似度最高且过阈值的才作为候选。新增药品不再需要重训模型只需要拍15到30张照片提取特征入库这个操作在手机上就能完成。提示如果只是做个玩具Demo分类方案完全可以但面向家庭长期使用度量学习的“可增量扩容”价值是决定性的我后来也靠这个特性让项目不用频繁发版也能维护药品库。3.3 置信度阈值与“我不确定”的输出药品识别的特殊性在于识别错误比识别不出的后果严重得多所以系统必须学会拒绝。我给相似度设了两道线相似度高于0.8才给唯一答案0.6到0.8之间给出Top3候选列表并提示“外观相近请核对包装”低于0.6直接显示“无法确认”。同时用温度缩放校准Embedding距离和概率之间的关系让0.8这个阈值更可信。还有一个几乎每个家庭项目都会踩的坑用户拿一颗维生素C咀嚼片来识别模型识别成了抗炎药。原因是非目标类别没有被建模。我在训练类别之外专门加了一个“非药片/未知物”类别包含纽扣、糖果、维生素软糖、宠物药等杂乱样本并在UI上提示“只能识别药片类固体不建议识别保健品”。这一步能挡掉大量不合理输入也大幅减少尴尬误报。4. 识别之后的事药品库映射与安全兜底识别模型说出“药片A”还不够它还要落到一个有意义的药品身份上用户才知道这是什么药、要不要谨慎处理。这一层我把它叫药品库映射做得好的话整个工具才有实际价值。4.1 通用名、商品名、厂商外观的三角关系药品命名有一个很误导人的现象同一个通用名比如阿莫西林可能有十几家药厂在生产各自商品名不同铝塑板颜色、药片外观也有差异。反过来同一商品名可能有很多规格比如缓释片和普通片外观相似但剂量释放方式完全不同。所以我的药品库不能只存“药名”而要存到SKU维度。实际表结构大概是这样的字段示例说明sku_idAMOX-500-01唯一主键通用名阿莫西林药物的化学通用名商品名阿莫仙品牌名规格500mg单粒剂量剂型胶囊剂型分类外观描述黄/红胶囊人类可读描述风险等级一般/高用于UI提示特征文件feat/amox_01.npy模型特征训练数据也按SKU组织而不是按药名组织。因为外观属于特定厂商的特定规格不绑定SKU的话模型学到的特征就会被不同外观的同名药搞乱。药品库里除了特征文件还保存一张经过药师确认的标准外观图。这个标准外观图很重要因为模型只负责输出相似特征人类可读的文字和图片介绍才是用户真正能信赖的界面。4.2 剂量易混淆品种的防错设计识别系统最怕一种情况两种药长得几乎一样药效却天差地别或者剂量不同。视觉上它们的颜色、形状、大小都可能接近模型在测试集上也许能靠微弱的印字区分但在真实暗光照片里大概率会混淆。我的防错逻辑是当相似度排序中Top1和Top2的差值小于预设阈值就认定“当前特征不足以区分”强制降级为多候选结果。系统同时显示两个药名并明确标出“两者外观接近请结合购买渠道或药盒核对”。另外对治疗窗窄的药物比如地高辛、华法林、部分降糖药我会在药品库里打上高风险标记。即使识别通过界面都会加一条醒目的“本结果仅供核对身份用药请遵医嘱”提示。这个设计不是为了免责是真实地想减少使用者被视觉相似误导的概率。因为这类药一旦吃错不是普通的“无效”或“副作用”问题而是直接造成风险UI上多停一下值得。4.3 家庭场景里还可以顺手做什么识别成功之后用户最想知道的三个问题一般是这是什么药、它是干什么用的、我该怎么吃。前两个好办药品库里有通用名和作用分类。第三个“怎么吃”涉及个体化剂量绝对不能由识别工具直接给答案。我在项目里展示的是“药品说明书摘要”而不是“用药方案”比如适应症分类、常见剂型信息全部链接到权威说明书来源不提供任何面向个人的剂量指导。方便性上我把识别结果和历史记录打通每次成功识别后生成一条记录包含时间、药品名、外观缩略图方便家人对照老人药盒里到底混了哪几种药。这个小功能业务逻辑不复杂但长辈用起来反馈很好相当于自动帮忙做了一次药物整理。用户还能给识别结果标记“正确/错误”这些反馈会回流到后台成为后续校准特征库的重要依据。5. 落地到手机端模型压缩与实测翻车记录模型在电脑上跑得再准到了中端安卓机上可能就是另一回事。这一章全是实打实的坑。5.1 目标硬件与框架选择我定义的目标设备很朴素一台一千多元的中端安卓手机骁龙6系级别4GB内存没有独立NPU算力。框架上Android端用TFLiteiOS端用Core ML公共推理接口封装成同一套。选TFLite主要是社区生态成熟YOLOv8转成TFLite的工具链比较顺EfficientNet-Lite本来就是为TFLite设计的。两个模型加起来检测模型INT8量化后约5.8MB特征提取模型约4.9MB整个特征库在几百个SKU场景下也只有几MB到十几MB手机启动加载完全无压力。关键指标是推理速度中端机CPU单帧全流程检测加特征提取实测30到60毫秒看起来不快但配合连续帧采样可以做到像实时扫描一样。我一开始还试过把两个模型合并成一个端到端大网络结果体积翻倍、可增量性全丢果断放弃。5.2 实测方法论与数据“能跑”和“能用”是两个概念。我建了一套独立的实测集每个品种至少15张包含统一灯箱、客厅日光、厨房暖光、暗光、运动模糊五种环境。指标不只看Top-1准确率更关注拒绝率和误识别率。下面是一版模型在6000多张测试图上的数据场景样本数Top-1准确率Top-3准确率拒绝率白底灯箱180096.8%98.5%1.0%客厅自然光150093.2%96.8%2.8%桌面暖光灯90089.5%94.2%6.1%暗光/手持微颤60074.3%86.1%19%半片/磨损40061.3%78.5%31%看到暗光和半片的数据就能明白为什么必须有不确认机制与其硬猜不如直接拒掉。我也是从这里开始把“准确率”之外的产品策略当成一等公民来设计而不是训练完再补一个阈值。5.3 三起典型误判复盘第一起半片药识别几乎全崩。药片被掰成半片后轮廓从圆形变成半圆检测框比例变化颜色占比也变了模型经常把它们判成小一号的其他药。修复思路是合成增强把完整药片照片按不同比例切出左半、右半、斜切、三分之一残片混合进训练集同时给这类样本单独取一个后缀标记推理时如果轮廓明显不完整就额外降低置信度。第二起包衣片反光导致颜色偏移。一粒红色缓释片在侧光下出现大面积白色高光模型把它识别成淡粉色同类虽然不致命但结果很晃眼。后来在训练里加了高光增强并在前端提示用户转一下角度再拍情况缓解很多。这个坑也说明药片颜色识别不能只依赖全局颜色统计模型需要学会忽略由光源产生的高光区域。第三起隔着铝塑板拍照。药片在透明泡罩里塑料曲面会产生畸变和反光尤其泡罩上还有铝箔背景检测框经常把整板药都框进去分类自然乱掉。我的处理是检测层增加“带泡罩”场景样本并明确在产品文案里建议用户把药片取出放在白纸上识别。说句实话这类图的最好解法是产品引导而不是硬练模型因为铝塑板反射变化太多训练样本永远覆盖不全。5.4 手机端连续识别的稳定性技巧单帧识别可以按一下拍一张但家庭用户更习惯把手机对准药片后等结果。连续识别模式下直接逐帧输出会导致结果抖动。我加了两层保护一是结果确认机制连续5帧都返回同一个SKU才显示“已确认”否则一直显示“请对准药片”二是检测框跟踪用上一帧的检测框缩小下一帧的搜索区域减少全图检测次数但搜索区域缩小到一定范围后必须重新全图检测避免跟丢后卡在错误位置。还有一个很省事的调优相机自动曝光在暗光下会把白平衡推到离谱的暖黄色导致颜色特征失效。我在相机初始化阶段强制锁定白平衡为“日光”或“自动但限定范围”对药片颜色识别稳定性提升非常明显。这个改动成本几乎为零效果却在暗光场景立竿见影。如果你在做一个依赖颜色的识别项目这个方法可以直接抄。6. 如果再让我重做一遍会有哪些不同项目做完整套链路后我回过头想有几个决定如果再选一次我会在一开始就做好省掉后面不少返工。第一从第一天就定义好“非药片类别”。我当时以为目标类别清晰结果被糖果、维生素软糖、纽扣折磨了很久。如果提前把垃圾类做进数据管道整个模型鲁棒性会高很多。这个类别不需要特别大几百张负面样本就能让模型学会拒绝不属于药品的对象。第二数据采集必须标准化。前期我用不同手机和背景拍了不少照片虽然多样性好但颜色真值很乱。后来引入色卡和统一灯箱后模型收敛速度和测试准确率都有明显提升。标准差不要省哪怕只是打印一张色卡也比靠眼睛估颜色强。数据采集流程最好在项目第一天就固定下来包括光源色温、拍摄角度、背景材质后面补数据时才不会出现新旧批次风格不一致的问题。第三评估指标里必须包含“拒绝率”。只看Top-1准确率很容易自我感觉良好但家庭场景里“识别不了”和“识别错”完全是两码事。把所有低置信度输出都算作错误才能真正逼着模型学会谨慎。我还建议在每周版本对比时固定同一个测试集同时看准确率和拒绝率的变化避免模型为了刷准确率而放弃对未知药品的拒识能力。第四UI提示要好过盲目优化模型。实测中有很多问题靠文案就能解决告诉用户取出药片、放白纸上、开灯拍识别率可以提升一截。产品引导和模型强是互补关系不要只想用算法弥补使用方法的缺失。我见过不少团队把大量精力花在提高边界样本准确率上其实只要一个“请将药片放在白色背景上”的提示就能解决一半问题。最后再分享一个小技巧如果你也在做类似识别项目可以在开发阶段把每次误判的图片单独归类每周复盘一次。我靠这个习惯找到了大量“模型没问题但标注错”的数据修正后测试集准确率直接涨了3个百分点。数据清洗的收益通常比换更强模型的收益来得快而且更稳定。