460 lines
18 KiB
Markdown
460 lines
18 KiB
Markdown
# 尿检试纸颜色识别功能 - 改造方案
|
||
|
||
> 创建时间: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_<uuid>.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'
|
||
// ...
|
||
<Route path="/admin/urinecheck" element={<PrivateRoute><UrineCheck /></PrivateRoute>} />
|
||
```
|
||
|
||
### 第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/ 可写 |
|
||
|
||
---
|
||
|
||
**请蔡叔叔审阅以上方案,确认后我开始写代码。有问题随时指出。**
|