bone_2026/src/quality/medical_quality.py

217 lines
9.1 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.

from typing import Dict, Any
import numpy as np
from monai.transforms.utils import ndimage
from src.quality.quality_scorer import QualityScorer
class MedicalQualityScorer(QualityScorer):
def __init__(self, anatomy_type="spine"):
super().__init__()
self.anatomy_type = anatomy_type # "spine", "femur", "whole_body"
def evaluate(self, image, segmentation) -> Dict[str, Any]:
# Базовые проверки
base = super().evaluate(image, segmentation)
# Специфические проверки
if self.anatomy_type == "spine":
medical_check = self.check_spine_segmentation(segmentation)
elif self.anatomy_type == "femur":
medical_check = self.check_femur_segmentation(segmentation)
return {
**base,
"medical_validation": medical_check
}
def check_spine_segmentation(self, segmentation: np.ndarray) -> Dict[str, Any]:
"""
Комплексная проверка сегментации позвоночника
Args:
segmentation: бинарная маска [H, W]
Returns:
Словарь с результатами проверки
"""
results = {
"valid": True,
"issues": [],
"metrics": {},
"recommendations": []
}
# 1. Проверка количества позвонков
labeled, num_features = ndimage.label(segmentation)
results["metrics"]["num_vertebrae"] = num_features
if num_features < 3:
results["valid"] = False
results["issues"].append({
"type": "TOO_FEW_VERTEBRAE",
"details": f"Найдено только {num_features} позвонков, ожидается минимум 3",
"severity": "HIGH"
})
results["recommendations"].append("Повторите исследование, захватите больше позвонков")
return results # Дальнейшие проверки не имеют смысла
# 2. Собираем центры позвонков
centers = []
sizes = []
for i in range(1, num_features + 1):
y, x = np.where(labeled == i)
if len(y) > 0:
centers.append((np.mean(y), np.mean(x)))
sizes.append(len(y))
# 3. Проверка выравнивания
x_coords = [c[1] for c in centers]
x_std = np.std(x_coords)
results["metrics"]["alignment_deviation"] = float(x_std)
if x_std > 20:
results["valid"] = False
results["issues"].append({
"type": "POOR_ALIGNMENT",
"details": f"Позвонки не выровнены (отклонение {x_std:.1f} пикселей)",
"severity": "MEDIUM"
})
results["recommendations"].append("Проверьте укладку пациента")
# 4. Проверка расстояний между позвонками
if len(centers) >= 2:
centers_sorted = sorted(centers, key=lambda c: c[0])
distances = []
for i in range(len(centers_sorted) - 1):
y1, _ = centers_sorted[i]
y2, _ = centers_sorted[i + 1]
distances.append(abs(y2 - y1))
if len(distances) > 0:
mean_dist = np.mean(distances)
std_dist = np.std(distances)
results["metrics"]["spacing_variation"] = float(std_dist / mean_dist)
if std_dist / mean_dist > 0.3:
results["valid"] = False
results["issues"].append({
"type": "IRREGULAR_SPACING",
"details": f"Неравномерное расстояние между позвонками",
"severity": "MEDIUM"
})
results["recommendations"].append("Возможно, пропущен позвонок или артефакт")
# 5. Проверка размера позвонков
if len(sizes) > 1:
mean_size = np.mean(sizes)
results["metrics"]["mean_vertebra_size"] = float(mean_size)
for i, size in enumerate(sizes):
if abs(size - mean_size) / mean_size > 0.5:
results["valid"] = False
results["issues"].append({
"type": "ABNORMAL_SIZE",
"details": f"Позвонок {i + 1} аномального размера",
"severity": "HIGH"
})
results["recommendations"].append("Проверьте сегментацию, возможно артефакт")
break
# Итоговая оценка
if not results["issues"]:
results["status"] = "GOOD"
results["recommendations"].append("Исследование выполнено качественно")
else:
results["status"] = "WARNING" if len(results["issues"]) <= 2 else "POOR"
return results
def check_spine_alignment(self, segmentation):
"""Проверка, что позвонки выровнены по вертикали"""
labeled, num_features = ndimage.label(segmentation)
# Собираем центры всех позвонков
centers = []
for i in range(1, num_features + 1):
y, x = np.where(labeled == i)
if len(y) > 0:
center_y = np.mean(y)
center_x = np.mean(x)
centers.append((center_y, center_x))
# Проверяем, что центры находятся примерно на одной вертикальной линии
x_coords = [c[1] for c in centers]
x_std = np.std(x_coords) # стандартное отклонение по X
# Если отклонение большое → позвоночник искривлен
# Если позвонки смещены влево-вправо → пациент лежал неправильно.
if x_std > 20: # порог в пикселях
return {
"valid": False,
"issue": "позвонки не выровнены",
"deviation": x_std,
"recommendation": "проверьте укладку пациента"
}
return {"valid": True, "alignment": "good"}
def check_vertebrae_spacing(self, centers):
"""Проверка равномерности расстояния между позвонками"""
# Сортируем по Y (сверху вниз)
centers_sorted = sorted(centers, key=lambda c: c[0])
# Вычисляем расстояния между соседними позвонками
distances = []
for i in range(len(centers_sorted) - 1):
y1, _ = centers_sorted[i]
y2, _ = centers_sorted[i + 1]
distances.append(abs(y2 - y1))
# Если расстояния сильно различаются → проблема
if len(distances) > 0:
mean_dist = np.mean(distances)
std_dist = np.std(distances)
if std_dist / mean_dist > 0.3: # больше 30% вариации
return {
"valid": False,
"issue": "неравномерное расстояние между позвонками",
"distances": distances,
"variation": std_dist / mean_dist
}
return {"valid": True}
def check_vertebrae_size(labeled, num_features):
"""Проверка, что все позвонки примерно одного размера"""
sizes = []
for i in range(1, num_features + 1):
size = np.sum(labeled == i)
sizes.append(size)
# Если один позвонок сильно отличается по размеру
if len(sizes) > 1:
mean_size = np.mean(sizes)
std_size = np.std(sizes)
# Проверяем каждый позвонок
issues = []
for i, size in enumerate(sizes):
if abs(size - mean_size) / mean_size > 0.5: # отклонение > 50%
issues.append({
"vertebra": i + 1,
"size": size,
"expected": mean_size,
"issue": "позвонок аномального размера"
})
if issues:
return {
"valid": False,
"issue": "обнаружены позвонки аномального размера",
"details": issues,
"recommendation": "возможно, артефакт или неправильная сегментация"
}
return {"valid": True}