217 lines
9.1 KiB
Python
217 lines
9.1 KiB
Python
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} |