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)