You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

130 lines
4.7 KiB

import numpy as np
def edge_angle_deg(p1, p2):
"Находит угол наклона отрезка p1→p2 в градусах (от -180 до +180)."
dx = float(p2[0] - p1[0])
dy = float(p2[1] - p1[1])
return np.degrees(np.arctan2(dy, dx))
def normalize_angle_minus90_90(a):
"Приводит угол к диапазону [-90, 90)."
return (a + 90.0) % 180.0 - 90.0
def angle_diff_deg(a, b):
"Возвращает насколько отличаются две линии по углу, но только 'острый' вариант (0..90)"
d = abs(a - b) % 180.0
if d > 90.0:
d = 180.0 - d
return d
def order_points_ccw(pts):
"Приводит 4 точки OBB к стабильному порядку по кругу"
pts = np.asarray(pts, dtype=np.float32)
c = pts.mean(axis=0)
ang = np.arctan2(pts[:, 1] - c[1], pts[:, 0] - c[0])
return pts[np.argsort(ang)]
def vertebra_endplates_angles(obb_pts):
"Нахождит углов верхней и нижней замыкательных пластинок"
p = order_points_ccw(obb_pts)
edges = []
for i in range(4):
a = p[i]
b = p[(i + 1) % 4]
ang = normalize_angle_minus90_90(edge_angle_deg(a, b))
mean_y = float((a[1] + b[1]) / 2.0)
score = abs(ang)
edges.append((score, mean_y, float(ang)))
edges.sort(key=lambda t: t[0])
e1, e2 = edges[0], edges[1]
top, bottom = (e1, e2) if e1[1] <= e2[1] else (e2, e1)
return top[2], bottom[2]
def fit_line_x_of_y(y, x, eps=1e-6):
"Строит прямую вида x = a*y + b по точкам"
if np.std(y) < eps:
return 0.0, float(np.mean(x))
a, b = np.polyfit(y, x, 1)
return float(a), float(b)
def fit_spine_axis_dx(vertebrae):
"""Строит глобальную ось позвоночника (одна прямая по всем центрам).
Возвращает dx: для каждого позвонка разницу показывая,
насколько позвонок левее/правее глобальной оси"""
centers = np.array([v["c"] for v in vertebrae], dtype=np.float32)
xs, ys = centers[:, 0], centers[:, 1]
a, b = fit_line_x_of_y(ys, xs)
return xs - (a * ys + b)
def fit_spine_axis_dx_local(vertebrae, window=7):
"Вычисляет локальные изгибы относительно соседей"
centers = np.array([v["c"] for v in vertebrae], dtype=np.float32)
xs, ys = centers[:, 0], centers[:, 1]
n = len(xs)
dx = np.zeros(n, dtype=np.float32)
half = window // 2
for i in range(n):
s = max(0, i - half)
e = min(n, i + half + 1)
a, b = fit_line_x_of_y(ys[s:e], xs[s:e])
dx[i] = xs[i] - (a * ys[i] + b)
return dx
def smooth_1d(x, iters=3, alpha=0.25):
"Простое сглаживание 1D-сигнала (dx)"
x = x.astype(np.float32).copy()
for _ in range(int(iters)):
x[1:-1] = x[1:-1] + alpha * (x[:-2] - 2 * x[1:-1] + x[2:])
return x
def fit_spine_axis_dx_endpoints(vertebrae):
"Строит ось как линию через первый и последний позвонок (а не через регрессию)"
centers = np.array([v["c"] for v in vertebrae], dtype=np.float32)
xs, ys = centers[:, 0], centers[:, 1]
x0, y0 = float(xs[0]), float(ys[0])
x1, y1 = float(xs[-1]), float(ys[-1])
if abs(y1 - y0) < 1e-6:
return xs - float(np.mean(xs))
t = (ys - y0) / (y1 - y0)
x_line = x0 + t * (x1 - x0)
return xs - x_line
def _smooth_chain(points, window=5):
"""сглаживает цепочку точек скользящим средним по окну window.
Делает окно нечётным, берёт среднее по соседям и возвращает сглаженные точки"""
pts = np.asarray(points, dtype=np.float32)
if len(pts) < 3 or window <= 2:
return pts
window = int(window)
if window % 2 == 0:
window += 1
half = window // 2
out = pts.copy()
for i in range(half, len(pts) - half):
out[i] = np.mean(pts[i - half:i + half + 1], axis=0)
return out
def arc_centerline(vertebrae, info, smooth_window=5):
"""Строит центральную линию дуги"""
s = info.get("upper_idx", info["start_idx"])
e = info.get("lower_idx", info["end_idx"])
if s > e:
s, e = e, s
centers = [vertebrae[k]["c"] for k in range(s, e + 1)]
if not centers:
return None
centers = np.array(centers, dtype=np.float32)
centers = centers[np.argsort(centers[:, 1])]
if smooth_window and smooth_window > 2:
centers = _smooth_chain(centers, window=smooth_window)
return centers