2025-05-27 15:46:31 +08:00

347 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)