# 尿检试纸颜色识别功能 - 改造方案 > 创建时间: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 重写的关键数据结构 ```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) ```python # 颜色误差 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型) ```python 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): ```json { "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" } ``` 失败响应: ```json { "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` 逻辑: 1. `class GlobalParams` — 所有常量 2. `class PaperConfig` — 试纸配置字典 3. `class ColorReader` — 白平衡 + 圆形采样 + HSV/LAB转换 4. `class UrineStripRecognizer` — 核心7步流程: - `preprocess_image()` - `detect_locators()` — 包括 HSV分割、轮廓查找、备选Canny - `filter_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_.jpg` (方便清理) - 调用 `UrineStripRecognizer.recognize()` - 如果成功,保存标注图到 `backend/uploads/urinecheck/` - 返回 JSON 结果 - `GET /api/urinecheck/result/{filename}` — 静态文件服务(标注图) ### 第3步: 注册路由 **修改** `backend/app.py`: ```python 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| ... │ │ │ └─────────────┘ │ └─────────────────────┘ │ ├──────────────────┴───────────────────────────┤ │ 标注图展示区 (色块中心红圈标记) │ │ ┌─────────────────────────────────────────┐ │ │ │ [标注后的试纸大图] │ │ │ └─────────────────────────────────────────┘ │ └──────────────────────────────────────────────┘ ``` 功能交互: 1. 用户上传图片 (拖拽或点击) 2. 图片立即显示预览(本地预览,无需等待) 3. 用户点击"开始识别"按钮 4. 前端调用 `POST /api/urinecheck/analyze` 5. 结果显示在右侧: - 基本信息(定位块颜色、试纸类型、清晰度) - 14项颜色值表格(RGB + HSV + LAB) - 标注图展示 ### 第5步: 修改导航 **文件**: `frontend/src/components-preline/Sidebar.jsx` — 在 `navItems` 中添加: ```javascript { path: '/admin/urinecheck', label: '尿检识别', icon: TestTubeIcon }, ``` ### 第6步: 修改路由 **文件**: `frontend/src/App.jsx` — 添加: ```jsx import { UrineCheck } from './pages/admin/UrineCheck' // ... } /> ``` ### 第7步: 测试验证 使用 `urinecheckcpp/images/` 下的测试图片验证: ```bash curl -X POST http://localhost:8889/api/urinecheck/analyze \ -F "file=@test1.png" ``` 验证: - 定位块正确识别 - 色块数量 = 14 - 颜色值合理 (RGB在0-255范围) - 标注图生成正确 --- ## 四、已确认的决定 1. ✅ **全部6种规格都支持** — 8联(S/L)、11联(S/L)、14联(S/L) 2. ✅ **输出中添加检测项目名称** — 14/11/8项都有对应的中文名称 3. ✅ **不需要标注图** — 只需颜色数据,移除 `draw_annotations()` 4. ✅ **不存数据库** — 每次临时识别,不上传文件到 uploads 目录 5. ✅ **上传图片存 /tmp** — 使用 `tempfile` 自动清理 6. ✅ **前端 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/ 可写 | --- **请蔡叔叔审阅以上方案,确认后我开始写代码。有问题随时指出。**