import os import json import cv2 import numpy as np try: from .back_data import BackData from .config import Config except: from back_data import BackData from config import Config from PIL import ImageFont, ImageDraw, Image class BackAcupointsDetector: def __init__(self): """初始化背部穴位检测器""" self.processor = None self.vertices = None # 背部区域四个顶点 self.back_center = None # 背部中心点坐标 self.dimensions = None # 背部尺寸信息 self.is_data_loaded = False self.coordinates = {} self.font_path = Config.get_font_path() # 初始化所有穴位配置 self._init_acupoints_config() # 计算中间变量 self.k_x = None self.k_y = None self.dazhui_point = None self.one_inch_x = None self.one_inch_y = None # 侧面穴位通用配置(穴位基础名, 距离基准点的寸数) self.side_configs = { '1.5': [ # 1.5寸侧线 ('大杼', 2), ('风门', 2.8), ('肺俞', 3.7), ('厥阴俞', 5), ('心俞', 6.1), ('督俞', 7.2), ('膈俞', 8.45), ('肝俞', 10.5), ('胆俞', 11.6), ('脾俞', 12.55), ('胃俞', 13.4), ('三焦俞', 14.3), ('肾俞', 15.3), ('气海俞', 16.4), ('大肠俞', 17.55), ('关元俞', 18.75), ('小肠俞', 19.5), ('膀胱俞', 20.45), ('中膂俞', 21.2), ('白环俞', 21.85) ], '3': [ # 3寸侧线 ("肩外俞", 0.9), ("曲垣", 2.1), ("附分", 2.8), ("魄户", 3.6), ("膏肓", 4.9), ("神堂", 6), ("譩譆", 7.15), ("膈关", 8.35), ("魂门", 10.4), ("阳纲", 11.45), ("意舍", 12.4), ("胃仓", 13.2), ("肓门", 14.0), ("志室", 15.1), ("胞肓", 20.4), ("秩边", 21.85) ], '0.75': [ # 0.75寸特殊穴位 ("会阳", 23.45) ], '2': [ # 2寸特殊穴位 ("肩中俞", 0.2) ], '4.5': [ # 4.5寸特殊穴位 ("京门", 14.5) ], '5.8': [ # 5.8寸特殊穴位 ("秉风", 2.2), ("天宗", 4.9) ] } def _init_acupoints_config(self): """初始化穴位配置""" # 中线穴位配置(穴位名, 距离大椎点的寸数) self.central_line_config = [ ("崇骨", -1), ("大椎", 0), ("陶道", 2), ("身柱", 3.75), ("神道", 6.1), ("灵台", 7.2), ("至阳", 8.45), ("筋缩", 10.51), ("中枢", 11.6), ("脊中", 12.55), ("悬枢", 14.3), ("命门", 15.3), ("腰阳关", 17.55) ] def load_data(self, json_data): """加载并处理背部数据""" self.processor = BackData(json_data) self.vertices = self.processor.get_vertices() self.back_center = self.processor.get_back_center_point() self.dimensions = self.processor.get_dimensions() self.is_data_loaded = True # 计算中间变量 zuo1, you1, zuo2 = self.vertices[0], self.vertices[1], self.vertices[3] self.k_x, _ = self._calculate_line_params(zuo1, you1) self.k_y, _ = self._calculate_line_params(zuo1, zuo2) self.dazhui_point = self._calculate_midpoint(zuo1, you1) self.one_inch_x = self.dimensions['length'] / 6 # self.one_inch_y = self.dimensions['length'] / 5.5 self.one_inch_y = self.dimensions['width'] / 15.3 return self def _calculate_line_params(self, point1, point2): """计算两点之间的斜率和截距""" x1, y1 = point1 x2, y2 = point2 if x1 == x2: return float('inf'), None k = (y2 - y1) / (x2 - x1) b = y1 - k * x1 return k, b def _calculate_midpoint(self, point1, point2): """计算两个点的中点""" return ((point1[0] + point2[0])/2, (point1[1] + point2[1])/2) def _calculate_shoulder_points(self, base_point, distances): """计算肩部基准点""" results = {} for distance_inch in distances: distance_pixel = distance_inch * self.one_inch_x x, y = base_point if self.k_x == 0: # 水平线 left = (x - distance_pixel, y) right = (x + distance_pixel, y) else: dx = 1 / ((1 + self.k_x**2)**0.5) dy = self.k_x * dx left = (x - distance_pixel * dx, y - distance_pixel * dy) right = (x + distance_pixel * dx, y + distance_pixel * dy) results[str(distance_inch)] = {'left': left, 'right': right} return results def _calculate_points_along_line(self, base_point, configs, direction_k): """沿指定方向计算穴位点""" points = {} base_x, base_y = base_point for name, distance_inch in configs: distance_pixel = distance_inch * self.one_inch_y if direction_k is None or abs(direction_k) > 1e6: # 垂直 new_x, new_y = base_x, base_y + distance_pixel elif abs(direction_k) < 1e-6: # 水平 new_x, new_y = base_x, base_y + distance_pixel else: dx = 1 / ((1 + direction_k**2)**0.5) dy = abs(direction_k) * dx if direction_k < 0: new_x = base_x - distance_pixel * dx else: new_x = base_x + distance_pixel * dx new_y = base_y + distance_pixel * dy points[name] = (new_x, new_y) return points def _calculate_side_acupoints(self, base_point, distance_key, side): """计算侧面穴位点""" side_suffix = "左" if side == "left" else "右" points = {} for name, dist in self.side_configs[distance_key]: if len(name) == 3 : full_name = f"{name[:-1]}{side_suffix}{name[-1]}" else: full_name = f"{name}{side_suffix}" # print(full_name) # 这里简化计算,实际应根据基准点和方向计算 points[full_name] = self._calculate_points_along_line( base_point, [(name, dist)], self.k_y )[name] return points # 画图 def plot_acupoints_on_image(self, input_image_path, coordinates, output_path): # 读取原始图像 orig_img = cv2.imread(input_image_path) if orig_img is None: raise FileNotFoundError(f"无法加载背景图:{input_image_path}") # 获取原始图像的尺寸 orig_h, orig_w = orig_img.shape[:2] # 定义目标分辨率(可以根据需求调整) # TARGET_W = 1280 # 目标宽度 TARGET_W = 1478 # 目标宽度 TARGET_H = int(orig_h * (TARGET_W / orig_w)) # 根据宽高比计算目标高度 # 将原始图像放大到目标尺寸 resized_img = cv2.resize(orig_img, (TARGET_W, TARGET_H), interpolation=cv2.INTER_LANCZOS4) # 计算缩放比例 scale_factor = TARGET_W / orig_w # 设置金色渐变颜色 golden_colors = [ (223, 87, 98), # 浅蓝紫色 (223, 87, 98), # 中等蓝紫色 (223, 87, 98), # 深蓝紫色 ] # 在放大后的图像上绘制穴位点 for name, (x, y) in coordinates.items(): if x == 0 and y == 0: continue # 将坐标缩放到目标尺寸 x, y = int(x * scale_factor), int(y * scale_factor) # 绘制金色穴位小球 radius = 5 # 小球半径 for r in range(radius, 0, -1): alpha = r / radius # alpha 从 1(中心)到 0(边缘) # 计算渐变颜色 color_index = int(alpha * (len(golden_colors) - 1)) color = golden_colors[color_index] # 绘制渐变圆 cv2.circle(resized_img, (x, y), r, color, -1, cv2.LINE_AA) # 添加高光效果 highlight_radius = int(radius * 0.45) # highlight_color = (255, 255, 255) # 白色高光 highlight_color = (0, 255, 185) # 白色高光 highlight_pos = (x, y) cv2.circle(resized_img, highlight_pos, highlight_radius, highlight_color, -1, cv2.LINE_AA) # 在放大后的图像上绘制文字 pil_img = Image.fromarray(cv2.cvtColor(resized_img, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(pil_img) # 直接在目标尺寸下设置字体大小 font_size = 12 font = ImageFont.truetype(self.font_path, font_size) for name, (x, y) in coordinates.items(): if x == 0 and y == 0: continue # 将坐标缩放到目标尺寸 x, y = int(x * scale_factor), int(y * scale_factor) if name in ["崇骨", "大椎", "陶道", "身柱", "神道", "灵台", "至阳", "筋缩", "中枢", "脊中", "悬枢", "命门","腰阳关"]: dx, dy = -font_size , -font_size * 1.8 elif name in [ "督俞左", "心俞左", "厥阴左俞", "肺俞左", "风门左", "大杼左", "膈俞左", "督俞右", "心俞右", "厥阴右俞", "肺俞右", "风门右", "大杼右", "膈俞右", "胃俞左", "脾俞左", "胆俞左", "肝俞左", "三焦左俞", "肾俞左", "胃俞右", "脾俞右", "胆俞右", "肝俞右", "三焦右俞", "肾俞右", "大肠左俞", "气海左俞", "大肠右俞", "气海右俞" ]: dx, dy = -font_size * 1.8, -font_size * 1.8 elif name == "白环左俞": dx, dy = -font_size * 1.8, font_size elif name == "白环右俞": dx, dy = -font_size * 1.8, font_size elif name == "膀胱左俞": dx, dy = -font_size * 0.2, -font_size * 1.8 elif name == "膀胱右俞": dx, dy = -font_size * 3, -font_size * 1.8 elif name in ["肩中左俞", "肩外左俞", "关元左俞","小肠左俞", "中膂左俞"]: dx, dy = -font_size * 5, -font_size * 0.8 elif "左" in name: dx, dy = -font_size * 4, -font_size * 0.8 else: dx, dy = font_size , -font_size * 0.8 # 绘制带白边的黑字 text_pos = (x + dx, y + dy) for offset in [(-1,-1), (-1,1), (1,-1), (1,1)]: draw.text((text_pos[0]+offset[0], text_pos[1]+offset[1]), name, font=font, fill=(255,255,255)) draw.text(text_pos, name, font=font, fill=(0,0,0)) # 转换回OpenCV格式并保存 final_img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) cv2.imwrite(output_path, final_img) def process_image(self, image_path, output_path): """处理图像并标记所有穴位点""" if not self.is_data_loaded: raise ValueError("请先加载数据") # 1. 计算所有基准点 shoulder_points = self._calculate_shoulder_points( self.dazhui_point, [0.75, 1.5, 2, 3, 4.5, 5.8] ) # 2. 计算中线穴位 self.coordinates.update(self._calculate_points_along_line( self.dazhui_point, self.central_line_config, self.k_y )) # 3. 计算侧面穴位 for distance in ['1.5', '3', '0.75', '2', '4.5', '5.8']: left_base = shoulder_points[distance]['left'] right_base = shoulder_points[distance]['right'] self.coordinates.update(self._calculate_side_acupoints( left_base, distance, "left" )) self.coordinates.update(self._calculate_side_acupoints( right_base, distance, "right" )) # 4. 绘制结果,保存图片 self.plot_acupoints_on_image(image_path, self.coordinates, output_path) return self.coordinates if __name__ == "__main__": sample_json = { "shapes": [ { "label": "back", "points": [ [332.0567375886525, 81.13475177304969], [405.81560283687946, 90.35460992907804], [378.86524822695037, 361.9858156028369], [302.2695035460993, 353.4751773049646] ], "shape_type": "polygon" }, { "label": "back_center", "points": [[341, 276]], "shape_type": "point" } ] } detector = BackAcupointsDetector().load_data(sample_json) # 计算并获取穴位 output_path = Config.get_output_path("back_acupoints.png") image_path = Config.get_image_path("color_735.png") acupoints = detector.process_image(image_path, output_path) print(acupoints)