337 lines
13 KiB
Python
337 lines
13 KiB
Python
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) |