431 lines
13 KiB
Python
431 lines
13 KiB
Python
"""
|
||
JSON数据尿检识别器 — 直接翻译 C++ ProcessJsonDataRequest + Get14Results
|
||
|
||
完整复制:
|
||
- UrineCheck_Class::ProcessJsonDataRequest (urinecheck_class.cpp:2052)
|
||
- UrineCheck14::Get14Results(mapData, X) (UrineCheck14.cpp:1041)
|
||
- 所有 Red*ResultIndex 修正函数 (UrineCheck14.cpp:1413-1746)
|
||
- PRO/MAU + SG/PH 关联修正逻辑
|
||
|
||
Author: 小月
|
||
Date: 2026-05-13
|
||
"""
|
||
|
||
import math
|
||
import logging
|
||
from collections import Counter
|
||
from typing import Dict, List, Optional, Tuple, Any
|
||
|
||
import numpy as np
|
||
|
||
logger = logging.getLogger("json_recognizer")
|
||
|
||
# 延迟导入(避免循环依赖)
|
||
_ml_config = None
|
||
|
||
|
||
def _get_config():
|
||
global _ml_config
|
||
if _ml_config is None:
|
||
from ml_config import MLConfigLoader
|
||
_ml_config = MLConfigLoader()
|
||
_ml_config.load()
|
||
return _ml_config
|
||
|
||
|
||
# =============================================================================
|
||
# 项目类型映射 (C++ getProjectType)
|
||
# =============================================================================
|
||
|
||
def get_project_type(paper_type: int) -> int:
|
||
"""paperType → projectType 映射"""
|
||
mapping = {
|
||
1: 10,
|
||
2: 2, 5: 2,
|
||
3: 1, 4: 1,
|
||
6: 9, 7: 9, 8: 9, 9: 9,
|
||
10: 7, 11: 7, 12: 7, 13: 7,
|
||
14: 8, 15: 8, 16: 8, 17: 8, 18: 8, 19: 8,
|
||
20: 20,
|
||
}
|
||
return mapping.get(paper_type, 0)
|
||
|
||
|
||
# =============================================================================
|
||
# 多帧投票 (C++ CountResultsModal)
|
||
# =============================================================================
|
||
|
||
def count_results_modal(results: Dict[int, List[int]]) -> Dict[int, int]:
|
||
"""对每个 position,取多帧结果中出现次数最多的 index(众数)"""
|
||
modal = {}
|
||
for position, indices in results.items():
|
||
if not indices:
|
||
continue
|
||
counter = Counter(indices)
|
||
most_common = counter.most_common(1)[0][0]
|
||
modal[position] = most_common
|
||
return modal
|
||
|
||
|
||
# =============================================================================
|
||
# 结果索引修正函数 (红色试纸, algorithm=0)
|
||
# 直接翻译 C++ UrineCheck14::GetRed*ResultIndex
|
||
# =============================================================================
|
||
|
||
def _safe_round(val) -> int:
|
||
"""safe round like C++ round()"""
|
||
if isinstance(val, np.ndarray):
|
||
val = float(val.item()) if val.size == 1 else float(val[0])
|
||
return int(round(float(val)))
|
||
|
||
|
||
def red_glu_index(hsv_lab, p) -> int:
|
||
return _safe_round(p)
|
||
|
||
def red_ket_index(hsv_lab, p) -> int:
|
||
ret = _safe_round(p)
|
||
h = float(hsv_lab[0])
|
||
if ret > 1 and 45 < h < 145:
|
||
return 0
|
||
return ret
|
||
|
||
def red_bil_index(hsv_lab, p) -> int:
|
||
return _safe_round(p)
|
||
|
||
def red_pro_index(hsv_lab, p) -> int:
|
||
ret = _safe_round(p)
|
||
h = float(hsv_lab[0])
|
||
if ret > 0 and (h < 25 or h > 110):
|
||
return 0
|
||
return ret
|
||
|
||
def red_nit_index(hsv_lab, p) -> int:
|
||
ret = _safe_round(p)
|
||
h = float(hsv_lab[0])
|
||
# C++ 20230731 version: h>40 && h<140
|
||
if ret > 0 and 40 < h < 140:
|
||
return 0
|
||
return ret
|
||
|
||
def red_ph_index(hsv_lab, p) -> int:
|
||
return _safe_round(p)
|
||
|
||
def red_bld_index(hsv_lab, p) -> int:
|
||
return _safe_round(p)
|
||
|
||
def red_sg_index(hsv_lab, p) -> int:
|
||
return _safe_round(p)
|
||
|
||
def red_wbc_index(hsv_lab, p) -> int:
|
||
return _safe_round(p)
|
||
|
||
def red_vc_index(hsv_lab, p) -> int:
|
||
return _safe_round(p)
|
||
|
||
def red_cre_index(hsv_lab, p) -> int:
|
||
return _safe_round(p)
|
||
|
||
def red_uca_index(hsv_lab, p) -> int:
|
||
return _safe_round(p)
|
||
|
||
def red_mau_index(hsv_lab, p) -> int:
|
||
ret = _safe_round(p)
|
||
h = float(hsv_lab[0])
|
||
if ret > 0 and (h < 30 or h > 115):
|
||
return 0
|
||
return ret
|
||
|
||
def red_uro_index(hsv_lab, p) -> int:
|
||
ret = _safe_round(p)
|
||
h = float(hsv_lab[0])
|
||
if ret > 0 and 25 < h < 150:
|
||
return 0
|
||
return ret
|
||
|
||
# ---- 绿色/蓝色试纸 (algorithm=1) ----
|
||
def glu_index(hsv_lab, p) -> int: return _safe_round(p)
|
||
def ket_index(hsv_lab, p) -> int: return _safe_round(p)
|
||
def bil_index(hsv_lab, p) -> int: return _safe_round(p)
|
||
def pro_index(hsv_lab, p) -> int: return _safe_round(p)
|
||
def nit_index(hsv_lab, p) -> int: return _safe_round(p)
|
||
def ph_index(hsv_lab, p) -> int: return _safe_round(p)
|
||
def bld_index(hsv_lab, p) -> int: return _safe_round(p)
|
||
def sg_index(hsv_lab, p) -> int: return _safe_round(p)
|
||
def wbc_index(hsv_lab, p) -> int: return _safe_round(p)
|
||
def vc_index(hsv_lab, p) -> int: return _safe_round(p)
|
||
def cre_index(hsv_lab, p) -> int: return _safe_round(p)
|
||
def uca_index(hsv_lab, p) -> int: return _safe_round(p)
|
||
def mau_index(hsv_lab, p) -> int: return _safe_round(p)
|
||
def uro_index(hsv_lab, p) -> int: return _safe_round(p)
|
||
|
||
# ---- PET试纸 (algorithm=2) ----
|
||
def pet_glu_index(hsv_lab, p) -> int: return _safe_round(p)
|
||
def pet_sg_index(hsv_lab, p) -> int: return _safe_round(p)
|
||
|
||
|
||
# 函数名 → 函数对象映射
|
||
RESULT_FUNC_REGISTRY = {
|
||
# Red (algorithm=0)
|
||
"red_glu": red_glu_index, "red_ket": red_ket_index,
|
||
"red_bil": red_bil_index, "red_pro": red_pro_index,
|
||
"red_nit": red_nit_index, "red_ph": red_ph_index,
|
||
"red_bld": red_bld_index, "red_sg": red_sg_index,
|
||
"red_wbc": red_wbc_index, "red_vc": red_vc_index,
|
||
"red_cre": red_cre_index, "red_uca": red_uca_index,
|
||
"red_mau": red_mau_index, "red_uro": red_uro_index,
|
||
# Green/Blue (algorithm=1)
|
||
"glu": glu_index, "ket": ket_index, "bil": bil_index,
|
||
"pro": pro_index, "nit": nit_index, "ph": ph_index,
|
||
"bld": bld_index, "sg": sg_index, "wbc": wbc_index,
|
||
"vc": vc_index, "cre": cre_index, "uca": uca_index,
|
||
"mau": mau_index, "uro": uro_index,
|
||
# PET (algorithm=2)
|
||
"pet_glu": pet_glu_index, "pet_sg": pet_sg_index,
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# 核心识别器
|
||
# =============================================================================
|
||
|
||
class UrineJsonRecognizer:
|
||
"""JSON数据尿检识别器"""
|
||
|
||
def __init__(self):
|
||
self.config = _get_config()
|
||
|
||
def recognize(self, json_body: dict) -> dict:
|
||
"""
|
||
完整识别流程 — 翻译 ProcessJsonDataRequest
|
||
|
||
输入: 与 txt 文件格式一致的 JSON dict
|
||
输出: {"score": "100", "status": "0", "data": [{id, index}, ...]}
|
||
"""
|
||
config = self.config
|
||
|
||
# ---- 1. 提取字段 ----
|
||
data_array = json_body.get("data", [])
|
||
extend = json_body.get("extend", {})
|
||
|
||
if not data_array or len(data_array) == 0:
|
||
return {
|
||
"score": "0", "status": "-8",
|
||
"message": "数据为空!", "project_id": 0, "imgid": 0,
|
||
}
|
||
|
||
# ---- 2. 确定试纸类型 ----
|
||
paper_type = self._extract_paper_type(extend)
|
||
project_id = get_project_type(paper_type)
|
||
|
||
if paper_type < 1 or paper_type >= 21:
|
||
return {
|
||
"score": "0", "status": "-10",
|
||
"message": "设置试纸类型参数错误!",
|
||
"project_id": paper_type, "imgid": 0,
|
||
}
|
||
|
||
X = paper_type - 1 # C++ 中的 papertypeIndex
|
||
|
||
# ---- 3. 逐帧处理 ----
|
||
results: Dict[int, List[int]] = {}
|
||
|
||
for frame in data_array:
|
||
if not isinstance(frame, list):
|
||
continue
|
||
|
||
# 3a. 解析颜色数据 → mapData
|
||
map_data = {}
|
||
for item in frame:
|
||
item_id = int(item.get("id", 0))
|
||
value_str = str(item.get("value", ""))
|
||
try:
|
||
values = [float(x) for x in value_str.split(",")]
|
||
except (ValueError, AttributeError):
|
||
continue
|
||
if len(values) >= 6:
|
||
map_data[item_id] = values[:6]
|
||
|
||
if not map_data:
|
||
continue
|
||
|
||
# 3b. 调用 ML 推理
|
||
frame_result = self._get14_results(map_data, X)
|
||
|
||
# 3c. 收集结果
|
||
for position, raw_index in frame_result.items():
|
||
if position not in results:
|
||
results[position] = []
|
||
results[position].append(raw_index)
|
||
|
||
if not results:
|
||
return {
|
||
"score": "0", "status": "-8",
|
||
"message": "未能识别任何色块!", "project_id": project_id, "imgid": 0,
|
||
}
|
||
|
||
# ---- 4. 多帧投票 ----
|
||
last_result = count_results_modal(results)
|
||
|
||
# ---- 5. PRO/MAU 关联修正 ----
|
||
pro_pos = config.positionsPro[X]
|
||
mau_pos = config.positionsMau[X]
|
||
pro_val = -1
|
||
mau_val = -1
|
||
max_mau = config.get_max_mau(X)
|
||
|
||
if pro_pos >= 0 and mau_pos >= 0:
|
||
if pro_pos in last_result:
|
||
pro_val = last_result[pro_pos]
|
||
if mau_pos in last_result:
|
||
mau_val = last_result[mau_pos]
|
||
|
||
if mau_val == 0:
|
||
pro_val = 0
|
||
else:
|
||
if pro_val == 0 and mau_val == max_mau:
|
||
mau_val = max_mau - 1
|
||
if pro_val >= 1:
|
||
mau_val = max_mau
|
||
|
||
# ---- 6. SG/PH 关联修正 ----
|
||
sg_pos = config.positionsSg[X]
|
||
ph_pos = config.positionsPh[X]
|
||
sg_val = -1
|
||
ph_val = -1
|
||
max_sg = config.get_max_sg(X)
|
||
|
||
if sg_pos >= 0 and ph_pos >= 0:
|
||
if sg_pos in last_result:
|
||
sg_val = last_result[sg_pos]
|
||
if ph_pos in last_result:
|
||
ph_val = last_result[ph_pos]
|
||
|
||
if project_id < 20 and ph_val >= 2 and sg_val < max_sg:
|
||
sg_val += 1
|
||
|
||
# ---- 7. 构建输出 data 数组 ----
|
||
output_data = []
|
||
for position in sorted(last_result.keys()):
|
||
raw_index = last_result[position]
|
||
output_id = config.get_stat_id(X, position)
|
||
|
||
# 获取最终 index
|
||
if pro_pos >= 0 and mau_pos >= 0:
|
||
if position == pro_pos:
|
||
raw_index = pro_val
|
||
elif position == mau_pos:
|
||
raw_index = mau_val
|
||
|
||
if sg_pos >= 0 and ph_pos >= 0:
|
||
if position == sg_pos:
|
||
raw_index = sg_val
|
||
|
||
# 通过 indexmap 映射
|
||
output_index = config.get_output_index(X, position, raw_index)
|
||
|
||
output_data.append({
|
||
"id": output_id,
|
||
"index": output_index,
|
||
})
|
||
|
||
return {
|
||
"score": "100",
|
||
"status": "0",
|
||
"message": "OK",
|
||
"project_id": project_id,
|
||
"paper_id": paper_type,
|
||
"imgid": 0,
|
||
"data": output_data,
|
||
}
|
||
|
||
# ========== ML 推理 ==========
|
||
|
||
def _get14_results(self, map_data: Dict[int, List[float]],
|
||
paper_type: int) -> Dict[int, int]:
|
||
"""
|
||
完整翻译 UrineCheck14::Get14Results(mapData, X)
|
||
|
||
对每个模型执行: 取6维特征 → RTrees预测 → 结果索引函数修正
|
||
"""
|
||
config = self.config
|
||
results = {}
|
||
|
||
for position, model in config.models[paper_type].items():
|
||
# 查找该 position 的颜色数据
|
||
if position not in map_data:
|
||
continue
|
||
|
||
vdata = map_data[position]
|
||
if len(vdata) < 6:
|
||
continue
|
||
|
||
# 构造 1×6 样本矩阵
|
||
sample = np.array([vdata[:6]], dtype=np.float32)
|
||
|
||
# RTrees 预测
|
||
try:
|
||
resp = model.predict(sample)
|
||
# resp 可能是 (retval, results) 元组或直接值
|
||
if isinstance(resp, tuple):
|
||
p = float(resp[1].item()) if resp[1].size == 1 else float(resp[1][0])
|
||
else:
|
||
p = float(resp)
|
||
except Exception as e:
|
||
logger.warning(f"预测失败 paperType={paper_type} pos={position}: {e}")
|
||
continue
|
||
|
||
# 获取结果索引函数
|
||
func_type = config.get_func_type(paper_type, position)
|
||
if func_type is None:
|
||
continue
|
||
|
||
func = RESULT_FUNC_REGISTRY.get(func_type)
|
||
if func is None:
|
||
continue
|
||
|
||
# 调用结果索引函数
|
||
try:
|
||
pi = func(vdata[:6], p)
|
||
results[position] = pi
|
||
except Exception as e:
|
||
logger.warning(f"索引函数调用失败 {func_type}: {e}")
|
||
continue
|
||
|
||
return results
|
||
|
||
# ========== 试纸类型提取 ==========
|
||
|
||
def _extract_paper_type(self, extend: dict) -> int:
|
||
"""从 extend 字段提取 paperType"""
|
||
if not isinstance(extend, dict) or not extend:
|
||
return 0
|
||
|
||
# type 优先
|
||
if "type" in extend:
|
||
try:
|
||
return int(extend["type"])
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
# color_14i 其次
|
||
if "color_14i" in extend:
|
||
try:
|
||
return int(extend["color_14i"])
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
return 0
|
||
|
||
|
||
# =============================================================================
|
||
# 全局单例
|
||
# =============================================================================
|
||
|
||
_recognizer: Optional[UrineJsonRecognizer] = None
|
||
|
||
|
||
def get_recognizer() -> UrineJsonRecognizer:
|
||
global _recognizer
|
||
if _recognizer is None:
|
||
_recognizer = UrineJsonRecognizer()
|
||
return _recognizer |