18 KiB
尿检试纸颜色识别功能 - 改造方案
创建时间:2026-05-03 基于:urinecheckcpp C++ 代码 (鸿蒙版) → OpenCV Python 重写 状态:方案讨论中,待确认后动工
一、现有 C++ 代码理解
1.1 试纸结构
┌─────────────────────────────────────────────────────────────────┐
│ 试纸物理结构 │
│ │
│ ┌────────┐ ┌──┐ │
│ │ 大定位块 │ [0][1][2][3][4][5][6][7][8][9][10][11][12][13] │ 小定位块 │
│ │ (10/15mm)│ ←── 14个反应色块 ──→ │ (3mm) │
│ └────────┘ └──┘ │
│ │
│ 定位块颜色: 红/绿/蓝 (两个定位块颜色必须相同) │
│ 大定位块: 10mm×5mm (S型, 宽高比2.0) 或 15mm×5mm (L型, 宽高比3.0) │
│ 小定位块: 3mm×5mm (宽高比≈1.667) │
│ 色块间距: 3.5个单位 (每单位2mm = 7mm间距) │
│ 第一个色块位置: 2.25个单位 (距离小定位块边缘4.5mm) │
└─────────────────────────────────────────────────────────────────┘
1.2 核心算法流程(7步)
| 步骤 | 名称 | 关键操作 |
|---|---|---|
| 1 | 图像预处理 | BGR/HSV/Gray转换, 高斯滤波(5×5) |
| 2 | 定位块检测 | HSV颜色分割(红/绿/蓝) → 形态学(CLOSE+OPEN, 9×9核) → 轮廓查找; 备选Canny边缘 |
| 3 | 候选筛选+配对 | 面积/宽高比/矩形度过滤 → Y坐标聚类 → 遍历配对验证(颜色一致+RGB距离<180+面积比±20%+Y差<4倍高度) |
| 4 | 清晰度评估 | Laplacian方差法, 分级: 清晰>300 / 轻微>100 / 严重<100 |
| 5 | 杂物检测 | 试纸高度≥30px, 候选数量≤15 |
| 6 | 规格匹配 | distRatio(定位块距离/试纸高度) + areaRatio → 6种规格之一 |
| 7 | 色块位置计算 | 边缘中心点 + 切片插值(slicesNum=50, firstPos=2.25, step=3.5) → 14个中心点 |
1.3 颜色读取
- 白平衡: 灰度世界算法(各通道均值缩放到相同目标值)
- 采样: 圆形掩码(半径3px)取均值
- 输出: RGB + HSV(H:0-360) + LAB(L:0-100, a/b:-128-127)
- 顺序: 始终从靠近小定位块的色块开始编号(id=0)
二、改造方案
2.1 整体架构
┌─────────────────────────────────────────────────────────┐
│ Frontend (React) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 新增页面: /admin/urinecheck │ │
│ │ ├── 图片上传 (拖拽/点击, 支持 jpg/png) │ │
│ │ ├── 上传预览 (显示原图) │ │
│ │ ├── 识别按钮 (调用后端API) │ │
│ │ ├── 标注图展示 (色块中心红圈标记) │ │
│ │ └── 结果输出框 (14项颜色值表格) │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ HTTP POST /api/urinecheck │
├─────────────────────────────────────────────────────────┤
│ Backend (FastAPI) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 新增路由: routers/urinecheck.py │ │
│ │ ├── POST /api/urinecheck/analyze │ │
│ │ │ 接收: multipart/form-data (image file) │ │
│ │ │ 返回: JSON { colors, annotated_image_url, ... } │ │
│ │ └── GET /api/urinecheck/result/{id} │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ 调用 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 算法模块: urine_recognizer.py (新建) │ │
│ │ ├── class UrineStripRecognizer │ │
│ │ ├── class ColorReader │ │
│ │ ├── class PaperConfig │ │
│ │ └── 全局参数常量 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
2.2 文件清单
personal-blog/
├── backend/
│ ├── routers/
│ │ └── urinecheck.py # 新增: API路由
│ ├── urine_recognizer.py # 新增: 核心识别算法 (Python重写)
│ ├── app.py # 修改: 注册新路由
│ └── requirements.txt # 已满足: opencv-python已安装
├── frontend/src/
│ ├── pages/admin/
│ │ └── UrineCheck.jsx # 新增: 尿检识别页面
│ ├── components-preline/
│ │ └── Sidebar.jsx # 修改: 添加导航项
│ ├── App.jsx # 修改: 添加路由
│ └── api/
│ └── urinecheck.js # 新增: API调用封装
└── docs/
└── urinecheck-plan.md # 本文件
2.3 Python 重写的关键数据结构
from dataclasses import dataclass, field
from enum import Enum
import cv2
import numpy as np
class LocatorColor(Enum):
UNKNOWN = 0
RED = 1
GREEN = 2
BLUE = 3
class BigLocatorType(Enum):
SHORT = 0 # 10mm
LONG = 1 # 15mm
class PaperSpatialType(Enum):
UNKNOWN = 0
PST_14_S = 1
PST_14_L = 2
PST_11_S = 3
PST_11_L = 4
PST_8_S = 5
PST_8_L = 6
@dataclass
class LocatorInfo:
rotated_rect: cv2.RotatedRect = None
center: tuple = (0, 0)
area: float = 0
width: float = 0
height: float = 0
color: LocatorColor = LocatorColor.UNKNOWN
is_big: bool = False
avg_color: tuple = (0, 0, 0)
@dataclass
class ColorResult:
id: int = 0
r: float = 0; g: float = 0; b: float = 0
h: float = 0; s: float = 0; v: float = 0
l: float = 0; a: float = 0; b_val: float = 0 # LAB
@dataclass
class RecognitionResult:
success: bool = False
error_code: int = -1
error_message: str = ""
paper_type: PaperSpatialType = PaperSpatialType.UNKNOWN
locator_color: LocatorColor = LocatorColor.UNKNOWN
item_count: int = 0
color_blocks: list = field(default_factory=list) # list of ColorResult
annotated_image: np.ndarray = None # 标注图 (BGR)
blur_level: int = 0 # 0=清晰, 1=轻微, 2=严重
blur_score: float = 0
2.4 全局参数 (直接平移 C++ GlobalParams)
# 颜色误差
COLOR_DISTANCE_THRESHOLD = 180.0
# 面积/距离/角度误差
AREA_ERROR_RATIO = 0.20
DISTANCE_ERROR_RATIO = 0.20
ANGLE_ERROR_DEGREE = 15.0
# 定位块宽高比
BIG_LOCATOR_RATIO_SHORT = 2.0 # 10mm: 10/5
BIG_LOCATOR_RATIO_LONG = 3.0 # 15mm: 15/5
SMALL_LOCATOR_RATIO = 1.667 # 5/3
# 面积比
AREA_RATIO_SHORT = 3.33 # 10/3
AREA_RATIO_LONG = 5.0 # 15/3
# 采样
MIN_SAMPLE_PIXELS = 25
DEFAULT_SAMPLE_RADIUS = 3
# 轮廓过滤
MIN_CONTOUR_AREA = 100.0
MAX_AREA_RATIO = 0.3
MIN_RECTANGULARITY = 0.70
# 定位块面积限制
MIN_SMALL_LOCATOR_AREA = 100.0
MAX_SMALL_LOCATOR_AREA = 8000.0
MIN_BIG_LOCATOR_AREA = 200.0
MAX_BIG_LOCATOR_AREA = 15000.0
2.5 试纸配置 (14联S型/L型)
PAPER_CONFIGS = {
PaperSpatialType.PST_14_S: {
"items_num": 14, "slices_num": 50,
"first_pos": 2.25, "step": 3.5, "dist_times": 21.3,
"wh_big_ratio": 2.0
},
PaperSpatialType.PST_14_L: {
"items_num": 14, "slices_num": 50,
"first_pos": 2.25, "step": 3.5, "dist_times": 21.8,
"wh_big_ratio": 3.0
},
}
2.6 API 设计
POST /api/urinecheck/analyze
请求:
multipart/form-data
- file: 试纸图片 (jpg/png)
成功响应 (200):
{
"success": true,
"paper_type": "PST_14_S",
"locator_color": "RED",
"item_count": 14,
"blur_level": 0,
"blur_score": 450.5,
"colors": [
{
"id": 0,
"rgb": {"r": 200, "g": 180, "b": 150},
"hsv": {"h": 36.0, "s": 64.0, "v": 200.0},
"lab": {"l": 75.0, "a": 5.0, "b": 18.0}
},
// ... 共14项
],
"annotated_image_url": "/uploads/urinecheck/annotated_xxx.jpg"
}
失败响应:
{
"success": false,
"error_code": 2,
"error_message": "No valid locator pair found"
}
GET /api/urinecheck/result/{filename} — 获取标注图
2.7 错误码体系 (与C++一致)
| 错误码 | 含义 |
|---|---|
| 0 | 成功 |
| 1 | 未找到轮廓/候选不足 |
| 2 | 未找到有效定位块配对 |
| 3 | 定位块颜色不一致/未知 |
| 4 | 试纸规格未匹配 |
| 5 | 色块位置计算失败 |
| 6 | 检测到多条试纸 |
| 7 | 输入图像为空 |
| 9 | 试纸过小(杂物) |
| 10 | 场景过于杂乱 |
三、实施步骤
第1步: 编写 Python 核心算法模块
文件: backend/urine_recognizer.py
完整平移 C++ PaperRecognizer + ColorReader + PaperConfig 逻辑:
class GlobalParams— 所有常量class PaperConfig— 试纸配置字典class ColorReader— 白平衡 + 圆形采样 + HSV/LAB转换class UrineStripRecognizer— 核心7步流程:preprocess_image()detect_locators()— 包括 HSV分割、轮廓查找、备选Cannyfilter_locator_candidates()— 多级过滤 + Y聚类 + 配对验证match_paper_spec()— distRatio/areaRatio 匹配calculate_color_block_centers()— 边缘中心 + 切片插值draw_annotations()— 色块红圈标记recognize()— 主入口
注意事项:
- Python OpenCV 中
cv2.findContours返回2个值(OpenCV 4.x),与C++ 不同 - 颜色空间: C++ BGR ↔ Python BGR 一致,无需转换
- 旋转矩形的
.points()/.boundingRect()函数签名略有不同 - 移除所有鸿蒙日志宏(PR_LOG_*),改用 Python
logging模块 - 移除
getPaperImg()/savePaperImg()— 博客项目不需要RGBA格式
第2步: 编写后端 API 路由
文件: backend/routers/urinecheck.py
POST /api/urinecheck/analyze- 接收上传的图片文件
- 保存到
/tmp/urinecheck_<uuid>.jpg(方便清理) - 调用
UrineStripRecognizer.recognize() - 如果成功,保存标注图到
backend/uploads/urinecheck/ - 返回 JSON 结果
GET /api/urinecheck/result/{filename}— 静态文件服务(标注图)
第3步: 注册路由
修改 backend/app.py:
from routers import urinecheck
app.include_router(urinecheck.router)
第4步: 编写前端页面
文件: frontend/src/pages/admin/UrineCheck.jsx
UI布局 (Preline风格,参考Images.jsx):
┌──────────────────────────────────────────────┐
│ 尿检识别 │
│ 上传尿常规试纸图片,自动识别14项颜色值 │
├──────────────────┬───────────────────────────┤
│ 上传区域 │ 结果输出区 │
│ ┌─────────────┐ │ ┌─────────────────────┐ │
│ │ 📤 拖拽或点击 │ │ │ 定位块颜色: RED │ │
│ │ 上传试纸图片 │ │ │ 试纸规格: 14联S型 │ │
│ │ jpg/png │ │ │ 清晰度: 清晰 (450) │ │
│ └─────────────┘ │ ├─────────────────────┤ │
│ │ │ # | RGB | HSV │ │
│ ┌─────────────┐ │ │ 0 | 200,180,150 | ... │ │
│ │ [预览图] │ │ │ 1 | ... │ │
│ │ │ │ │ ... │ │
│ │ │ │ │ 13| ... │ │
│ └─────────────┘ │ └─────────────────────┘ │
├──────────────────┴───────────────────────────┤
│ 标注图展示区 (色块中心红圈标记) │
│ ┌─────────────────────────────────────────┐ │
│ │ [标注后的试纸大图] │ │
│ └─────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
功能交互:
- 用户上传图片 (拖拽或点击)
- 图片立即显示预览(本地预览,无需等待)
- 用户点击"开始识别"按钮
- 前端调用
POST /api/urinecheck/analyze - 结果显示在右侧:
- 基本信息(定位块颜色、试纸类型、清晰度)
- 14项颜色值表格(RGB + HSV + LAB)
- 标注图展示
第5步: 修改导航
文件: frontend/src/components-preline/Sidebar.jsx — 在 navItems 中添加:
{ path: '/admin/urinecheck', label: '尿检识别', icon: TestTubeIcon },
第6步: 修改路由
文件: frontend/src/App.jsx — 添加:
import { UrineCheck } from './pages/admin/UrineCheck'
// ...
<Route path="/admin/urinecheck" element={<PrivateRoute><UrineCheck /></PrivateRoute>} />
第7步: 测试验证
使用 urinecheckcpp/images/ 下的测试图片验证:
curl -X POST http://localhost:8889/api/urinecheck/analyze \
-F "file=@test1.png"
验证:
- 定位块正确识别
- 色块数量 = 14
- 颜色值合理 (RGB在0-255范围)
- 标注图生成正确
四、已确认的决定
- ✅ 全部6种规格都支持 — 8联(S/L)、11联(S/L)、14联(S/L)
- ✅ 输出中添加检测项目名称 — 14/11/8项都有对应的中文名称
- ✅ 不需要标注图 — 只需颜色数据,移除
draw_annotations() - ✅ 不存数据库 — 每次临时识别,不上传文件到 uploads 目录
- ✅ 上传图片存 /tmp — 使用
tempfile自动清理 - ✅ 前端 Preline UI 风格 — 与现有管理后台一致
五、检测项目名称映射
14联 (id=0 靠近小定位块)
| id | 项目 | id | 项目 |
|---|---|---|---|
| 0 | 微量白蛋白(MA) | 7 | 比重(SG) |
| 1 | 钙(Ca) | 8 | 潜血(BLD) |
| 2 | 肌酐(CRE) | 9 | pH |
| 3 | 抗坏血酸(VC) | 10 | 蛋白质(PRO) |
| 4 | 葡萄糖(GLU) | 11 | 尿胆原(URO) |
| 5 | 胆红素(BIL) | 12 | 亚硝酸盐(NIT) |
| 6 | 酮体(KET) | 13 | 白细胞(LEU) |
11联
| id | 项目 | id | 项目 |
|---|---|---|---|
| 0 | 抗坏血酸(VC) | 6 | pH |
| 1 | 葡萄糖(GLU) | 7 | 蛋白质(PRO) |
| 2 | 胆红素(BIL) | 8 | 尿胆原(URO) |
| 3 | 酮体(KET) | 9 | 亚硝酸盐(NIT) |
| 4 | 比重(SG) | 10 | 白细胞(LEU) |
| 5 | 潜血(BLD) |
8联
| id | 项目 | id | 项目 |
|---|---|---|---|
| 0 | 葡萄糖(GLU) | 4 | 潜血(BLD) |
| 1 | 胆红素(BIL) | 5 | pH |
| 2 | 酮体(KET) | 6 | 蛋白质(PRO) |
| 3 | 比重(SG) | 7 | 白细胞(LEU) |
⚠️ 注意:项目名称根据常见尿试纸排列推定,不同品牌/批次可能顺序不同。 建议对照实际试纸包装确认顺序。代码中的映射在
urine_recognizer.py的ITEM_NAMES_*中定义。
六、技术风险与注意事项
| OpenCV 版本差异 | Python OpenCV API与C++有细微差异 | 已在代码审查中标注关键差异点 | | 颜色分割阈值 | 不同光照下HSV阈值需要微调 | 直接复用C++验证过的阈值 | | 性能 | Python可能比C++慢 | 单张图片秒级处理,可接受 | | 图片格式 | 用户可能上传超大图片 | 限制最大10MB,压缩到合理尺寸 | | systemd 写入限制 | 如遇ReadWritePaths限制 | 确保 /tmp 和 backend/uploads/ 可写 |
请蔡叔叔审阅以上方案,确认后我开始写代码。有问题随时指出。