347 lines
12 KiB
Python
347 lines
12 KiB
Python
import math
|
||
import cv2
|
||
import numpy as np
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
import json
|
||
import os
|
||
import sys
|
||
import math
|
||
try:
|
||
from .leg_data import LegData
|
||
from .config import Config
|
||
except:
|
||
from leg_data import LegData
|
||
from config import Config
|
||
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||
sys.path.append(parent_dir)
|
||
from tools.log import CustomLogger
|
||
|
||
class LegAcupointsDetector:
|
||
def __init__(self):
|
||
self.logger = CustomLogger(log_name="LegAcupointsDetector",propagate=True)
|
||
self.font_path = Config.get_font_path()
|
||
self.ratios = {
|
||
"thigh": {
|
||
"殷门": 2/5,
|
||
"上委中": 5.5/7
|
||
},
|
||
"calf": {
|
||
"承山": 0.4,
|
||
"承筋": 0.65,
|
||
"合阳": 0.85
|
||
}
|
||
}
|
||
|
||
def load_data(self, json_data):
|
||
"""加载并处理背部数据"""
|
||
self.leg_processor = LegData(json_data)
|
||
self.is_data_loaded = True
|
||
return self
|
||
|
||
def distance(self, p1, p2):
|
||
return math.hypot(p1[0] - p2[0], p1[1] - p2[1])
|
||
|
||
def validate_anatomy(self, points, min_distance=50):
|
||
# 承扶左 和 委中左
|
||
if self.distance(points["承扶左"], points["委中左"]) < min_distance:
|
||
self.logger.log_warning("承扶和委中很接近,提示:躺得太上了")
|
||
return None
|
||
|
||
# 承扶右 和 委中右
|
||
if self.distance(points["承扶右"], points["委中右"]) < min_distance:
|
||
self.logger.log_warning("承扶和委中很接近,提示:躺得太上了")
|
||
return None
|
||
|
||
# 委中左 和 昆仑左
|
||
if self.distance(points["委中左"], points["昆仑左"]) < min_distance:
|
||
self.logger.log_warning("委中和昆仑很接近,提示:躺得太下了")
|
||
return None
|
||
|
||
# 委中右 和 昆仑右
|
||
if self.distance(points["委中右"], points["昆仑右"]) < min_distance:
|
||
self.logger.log_warning("委中和昆仑很接近,提示:躺得太下了")
|
||
return None
|
||
return True
|
||
|
||
def compute_control_points(self, start, mid, end, curvature=0):
|
||
"""计算贝塞尔曲线的控制点"""
|
||
mid_x = (start[0] + end[0]) / 2
|
||
mid_y = (start[1] + end[1]) / 2
|
||
|
||
dx = end[0] - start[0]
|
||
dy = end[1] - start[1]
|
||
normal = (-dy, dx)
|
||
norm = math.hypot(*normal)
|
||
|
||
if norm < 1e-6:
|
||
return [start, mid, end]
|
||
|
||
ctrl_point = (
|
||
mid_x + (normal[0]/norm) * curvature,
|
||
mid_y + (normal[1]/norm) * curvature
|
||
)
|
||
|
||
return [start, ctrl_point, end]
|
||
|
||
def bezier_interpolation(self, points, t):
|
||
"""基于贝塞尔曲线的插值"""
|
||
if len(points) == 2:
|
||
x = (1 - t) * points[0][0] + t * points[1][0]
|
||
y = (1 - t) * points[0][1] + t * points[1][1]
|
||
else:
|
||
x = (1 - t)**2 * points[0][0] + 2 * (1 - t) * t * points[1][0] + t**2 * points[2][0]
|
||
y = (1 - t)**2 * points[0][1] + 2 * (1 - t) * t * points[1][1] + t**2 * points[2][1]
|
||
|
||
return (int(x), int(y))
|
||
|
||
def generate_bezier_trajectory(self, points, num_points=100):
|
||
"""生成贝塞尔曲线轨迹"""
|
||
trajectory = []
|
||
for t in np.linspace(0, 1, num_points):
|
||
point = self.bezier_interpolation(points, t)
|
||
trajectory.append(point)
|
||
return trajectory
|
||
|
||
def predict_meridians(self, base_points, curvatures=None):
|
||
"""经络预测主算法 - 基于贝塞尔曲线"""
|
||
|
||
if curvatures is None:
|
||
curvatures = {
|
||
"left": {
|
||
"thigh": 30,
|
||
"calf": 25
|
||
},
|
||
"right": {
|
||
"thigh": -25,
|
||
"calf": -20
|
||
}
|
||
}
|
||
|
||
meridians = base_points.copy()
|
||
|
||
for side in ["左", "右"]:
|
||
side_key = "left" if side == "左" else "right"
|
||
|
||
thigh_control = self.compute_control_points(
|
||
base_points[f"承扶{side}"],
|
||
base_points[f"委中{side}"],
|
||
base_points[f"委中{side}"],
|
||
curvatures[side_key]["thigh"]
|
||
)
|
||
|
||
calf_control = self.compute_control_points(
|
||
base_points[f"昆仑{side}"],
|
||
base_points[f"委中{side}"],
|
||
base_points[f"委中{side}"],
|
||
curvatures[side_key]["calf"]
|
||
)
|
||
|
||
for name, ratio in self.ratios["thigh"].items():
|
||
meridians[f"{name}{side}"] = self.bezier_interpolation(
|
||
thigh_control, ratio
|
||
)
|
||
|
||
for name, ratio in self.ratios["calf"].items():
|
||
meridians[f"{name}{side}"] = self.bezier_interpolation(
|
||
calf_control, ratio
|
||
)
|
||
|
||
meridians[f"thigh_trajectory_{side}"] = self.generate_bezier_trajectory(thigh_control)
|
||
meridians[f"calf_trajectory_{side}"] = self.generate_bezier_trajectory(calf_control)
|
||
|
||
return meridians
|
||
|
||
def visualize_meridians(self, image_path, points, output_path, scale=2):
|
||
"""可视化引擎 - 展示贝塞尔曲线轨迹"""
|
||
# 读取并缩放图像
|
||
base_img = cv2.imread(image_path)
|
||
if base_img is None:
|
||
raise FileNotFoundError(f"找不到背景图: {image_path}")
|
||
|
||
# 缩放图像
|
||
highres_img = cv2.resize(base_img, None, fx=scale, fy=scale,
|
||
interpolation=cv2.INTER_LINEAR)
|
||
|
||
# 定义蓝紫色渐变颜色BGR
|
||
purple_colors = [
|
||
(223, 87, 98), # 浅蓝紫色
|
||
(223, 87, 98), # 中等蓝紫色
|
||
(223, 87, 98),
|
||
]
|
||
|
||
# 绘制曲线
|
||
for side in ["左", "右"]:
|
||
thigh_traj = points[f"thigh_trajectory_{side}"]
|
||
scaled_thigh_traj = [(int(x * scale), int(y * scale)) for x, y in thigh_traj]
|
||
cv2.polylines(highres_img, [np.array(scaled_thigh_traj)], False, (220, 240, 255), 1, cv2.LINE_AA)
|
||
|
||
calf_traj = points[f"calf_trajectory_{side}"]
|
||
scaled_calf_traj = [(int(x * scale), int(y * scale)) for x, y in calf_traj]
|
||
cv2.polylines(highres_img, [np.array(scaled_calf_traj)], False, (220, 240, 255), 1, cv2.LINE_AA)
|
||
|
||
# 先绘制所有OpenCV元素(曲线和圆圈)
|
||
for name, point in points.items():
|
||
if "trajectory" in name:
|
||
continue
|
||
|
||
x, y = point
|
||
x_scaled, y_scaled = int(x * scale), int(y * scale)
|
||
|
||
# 绘制蓝紫色带光圈的穴位点
|
||
radius = 4 # 穴位点半径
|
||
for r in range(radius, 0, -1):
|
||
alpha = r / radius # alpha 从 1(中心)到 0(边缘)
|
||
color_index = int(alpha * (len(purple_colors) - 1))
|
||
color = purple_colors[color_index]
|
||
cv2.circle(highres_img, (x_scaled, y_scaled), r, color, -1, cv2.LINE_AA)
|
||
|
||
# 添加高光效果
|
||
highlight_radius = int(radius * 0.6)
|
||
highlight_color = (0, 255, 185) # 白色高光
|
||
cv2.circle(highres_img, (x_scaled, y_scaled), highlight_radius, highlight_color, -1, cv2.LINE_AA)
|
||
|
||
# 转换为PIL图像用于绘制文本
|
||
canvas = Image.fromarray(cv2.cvtColor(highres_img, cv2.COLOR_BGR2RGB))
|
||
drawer = ImageDraw.Draw(canvas)
|
||
font = ImageFont.truetype(self.font_path, int(8 * scale)) # 调整为参考代码的字体大小
|
||
|
||
# 绘制文本
|
||
for name, point in points.items():
|
||
if "trajectory" in name:
|
||
continue
|
||
|
||
x, y = point
|
||
x_scaled, y_scaled = int(x * scale), int(y * scale)
|
||
|
||
# 确定文本偏移量
|
||
if name == "上委中左":
|
||
x_offset, y_offset = -40 * scale, -5 * scale
|
||
elif "左" in name:
|
||
x_offset, y_offset = -30 * scale, -5 * scale
|
||
else:
|
||
x_offset, y_offset = 5 * scale, -5 * scale
|
||
|
||
# 绘制带白边的黑字
|
||
text_pos = (x_scaled + x_offset, y_scaled + y_offset)
|
||
for offset in [(-1,-1), (-1,1), (1,-1), (1,1)]:
|
||
drawer.text((text_pos[0]+offset[0], text_pos[1]+offset[1]),
|
||
name, font=font, fill=(255,255,255)) # 白边
|
||
drawer.text(text_pos, name, font=font, fill=(0,0,0)) # 黑字
|
||
|
||
# 转换回OpenCV格式并保存
|
||
result_img = cv2.cvtColor(np.array(canvas), cv2.COLOR_RGB2BGR)
|
||
cv2.imwrite(output_path, result_img)
|
||
self.logger.log_info(f"可视化结果已保存到: {output_path}")
|
||
|
||
def build_point_mapping_and_curvatures(self):
|
||
"""
|
||
构建穴位坐标映射和曲率字典
|
||
|
||
Args:
|
||
leg_processor: LegData实例
|
||
|
||
Returns:
|
||
tuple: (point_mapping, curvatures)
|
||
"""
|
||
point_mapping = {
|
||
'承扶左': self.leg_processor.get_left_point1(),
|
||
'委中左': self.leg_processor.get_left_point2(),
|
||
'昆仑左': self.leg_processor.get_left_point3(),
|
||
'承扶右': self.leg_processor.get_right_point1(),
|
||
'委中右': self.leg_processor.get_right_point2(),
|
||
'昆仑右': self.leg_processor.get_right_point3()
|
||
}
|
||
|
||
curvatures = {
|
||
"left": self.leg_processor.get_left_curvatures(),
|
||
"right": self.leg_processor.get_right_curvatures()
|
||
}
|
||
|
||
return point_mapping, curvatures
|
||
|
||
def process_image(self, image_path, output_path):
|
||
# 构建映射字典
|
||
if not self.is_data_loaded:
|
||
raise ValueError("请先加载数据")
|
||
# 构建基本数据字典
|
||
point_mapping, curvatures = self.build_point_mapping_and_curvatures()
|
||
error_result = self.validate_anatomy(point_mapping)
|
||
if error_result is None:
|
||
return None
|
||
result = self.predict_meridians(point_mapping, curvatures)
|
||
|
||
# 如果预测成功,执行可视化
|
||
if result:
|
||
self.visualize_meridians(image_path,result,output_path)
|
||
acupoints = {k: v for k, v in result.items() if "trajectory" not in k}
|
||
|
||
return acupoints
|
||
|
||
# 主程序
|
||
if __name__ == "__main__":
|
||
|
||
sample_json = {
|
||
"shapes": [
|
||
{
|
||
"label": "C1",
|
||
"points": [
|
||
[
|
||
258,
|
||
2.0
|
||
],
|
||
[
|
||
265,
|
||
130
|
||
],
|
||
[
|
||
281,
|
||
315
|
||
]
|
||
],
|
||
"curvatures": {
|
||
"thigh": 10,
|
||
"calf": -10
|
||
},
|
||
"shape_type": "linestrip"
|
||
},
|
||
{
|
||
"label": "C2",
|
||
"points": [
|
||
[
|
||
350,
|
||
1.55
|
||
],
|
||
[
|
||
362,
|
||
110
|
||
],
|
||
[
|
||
401,
|
||
302
|
||
]
|
||
],
|
||
"curvatures": {
|
||
"thigh": -10,
|
||
"calf": 20
|
||
},
|
||
"shape_type": "linestrip"
|
||
}
|
||
]
|
||
}
|
||
|
||
# 指定JSON文件路径
|
||
# json_file_path = "/home/kira/codes/leg_test/leg_Json/d2025331_color199.json" # 请替换为实际路径,例如 "C:/data/keypoints.json"
|
||
|
||
# with open(json_file_path, 'r', encoding='utf-8') as f:
|
||
# sample_data = json.load(f)
|
||
|
||
# 初始化系统
|
||
detector = LegAcupointsDetector().load_data(sample_json)
|
||
|
||
# 计算并获取穴位
|
||
output_path = Config.get_output_path("leg_acupoints.png")
|
||
image_path = Config.get_image_path("color_199.png")
|
||
acupoints = detector.process_image(image_path, output_path)
|
||
print(acupoints)
|
||
|