diff --git a/README.md b/README.md
index 7a9478a..7537bd1 100644
--- a/README.md
+++ b/README.md
@@ -1,144 +1,154 @@
-# 🦴 Bone Quality Assessment
+# 🦴 DXA Quality Assessment
-[](https://www.python.org/)
-[](https://fastapi.tiangolo.com/)
-[](https://pytorch.org/)
-[](https://www.docker.com/)
+[](https://www.python.org/)
+[](https://fastapi.tiangolo.com/)
+[](https://pytorch.org/)
-## 📋 Описание
+## Описание
-Сервис искусственного интеллекта для автоматизированной оценки качества денситометрических изображений и их разметки. Система получает на вход рентгеновское денситометрическое исследование в формате DICOM, и оценивает качество выполнения исследования по стандартным критериям, а также корректность разметки анатомических структур на изображениях.
+Сервис для автоматизированной оценки качества денситометрических исследований (DXA). Система анализирует DICOM-изображения костной денситометрии и определяет качество исследования по следующим критериям:
+
+- **Артефакты** — движение, размытость, металлические объекты, имплантаты
+- **Позиционирование** — правильное расположение анатомической области в кадре
+- **Полнота изображения** — видимость всех анатомических структур (позвонки L1-L4, бедро)
+- **Ротация** — корректный угол поворота (для исследования бедра)
+- **ROI-валидация** — правильность расположения области интереса
### Основные возможности
-- 🖼️ **Анализ изображений** — загрузка и обработка медицинских изображений
-- 🧠 **Сегментация объектов** — выделение анатомических структур (позвонки, кости)
-- 📊 **Оценка качества** — проверка по 3 критериям:
- - Артефакты (движение, шум, размытость)
- - Позиционирование (правильное расположение объекта)
- - Контрастность (качество изображения)
-- 🔍 **Детекция нарушений** — определение типа нарушения для некачественных исследований
-- 🌐 **Web-интерфейс** — удобная загрузка и визуализация результатов
-- 📡 **REST API** — интеграция с внешними системами
-- 📈 **Визуализация маски** — отображение сегментации на изображении
+- 🔬 **Анализ DICOM** — загрузка и обработка медицинских изображений
+- 🧠 **Классификация** — бинарная оценка качества (OK / Violation)
+- 🔍 **Детекция нарушений** — определение типа нарушения:
+ - `correct` — качество соответствует норме
+ - `artifact_motion` — артефакт движения
+ - `artifact_other` — прочие артефакты
+ - `position_error` — ошибка позиционирования
+ - `rotation` — нарушение ротации
+ - `incomplete_view` — неполный вид
+ - `roi_error` — ошибка ROI
+ - `labeling_error` — ошибка разметки
+- 🌐 **REST API** — интеграция с внешними системами
+- 📊 **Веб-интерфейс** — загрузка и визуализация результатов
+- 📈 **Экспорт** — выгрузка результатов в XLSX
-## 🏗️ Архитектура
+---
-
+## Архитектура
-## 🎯 Доступные режимы
-
-### DXA Режим (хакатон)
-Анализ денситометрических исследований:
-```bash
-# Обучение
-python src/dxa/train.py --epochs 10
-
-# Инференс
-python src/dxa/inference.py --input-path dataset_hack/Для\ теста --output-path results.xlsx
+```
+┌─────────────────────────────────────────────────────────────┐
+│ FastAPI Server │
+│ (port 8000) │
+├─────────────────────────────────────────────────────────────┤
+│ /api/v1/analyze → Basic quality prediction │
+│ /api/v1/analyze/detailed → Full report with metrics │
+│ /api/v1/analyze/sr → DICOM SR (Structured Report) │
+│ /api/v1/batch → Batch processing │
+│ /api/v1/export → XLSX export │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ ┌──────────────┐ ┌─────────────────┐ │
+│ │ ResNet18 │───▶│ Quality Model │ │
+│ │ (pretrained) │ │ (binary class) │ │
+│ └──────────────┘ └────────┬────────┘ │
+│ │ │
+│ ▼ │
+│ ┌─────────────────────┐ │
+│ │ Detailed Assessment │ │
+│ │ - Motion detection │ │
+│ │ - Artifact detection│ │
+│ │ - ROI validation │ │
+│ │ - View completeness│ │
+│ └─────────────────────┘ │
+└─────────────────────────────────────────────────────────────┘
```
+---
-## 🚀 Быстрый старт
+## Быстрый старт
### Требования
- Python 3.10+
-- PyTorch 2.1+
-- Docker (опционально)
+- PyTorch 2.0+
+- 4GB+ RAM
+- (опционально) GPU CUDA/MPS для ускорения
-### Локальная установка
+### Установка
```bash
-# 1. Клонирование репозитория
+# Клонирование
git clone https://github.com/yourusername/bone-quality-assessment.git
cd bone-quality-assessment
-# 2. Создание виртуального окружения
+# Создание виртуального окружения
python -m venv venv
source venv/bin/activate # Linux/Mac
-# или
-venv\Scripts\activate # Windows
+# venv\Scripts\activate # Windows
-# 3. Установка зависимостей
+# Установка зависимостей
pip install -r requirements.txt
-# 4. Загрузка обученной модели (опционально)
-# Поместите модель в папку models/dxa_model.pth
+# Загрузка модели (опционально)
+# Поместите файл модели в models/dxa_model.pth
+```
-# 5. Запуск сервера
+### Запуск сервера
+
+```bash
+# Локальный запуск
+python -m uvicorn src.main:app --host 0.0.0.0 --port 8000
+
+# Или через run.py
python run.py
```
-Docker
+После запуска:
+- Web-интерфейс: http://localhost:8000
+- Swagger UI: http://localhost:8000/docs
+- ReDoc: http://localhost:8000/redoc
+
+### Docker
```bash
-# 1. Сборка образа
-docker build -t bone-quality-api .
+# Сборка
+docker build -t dxa-quality-api .
-# 2. Запуск контейнера
-docker run -p 8000:8000 bone-quality-api
-
-# 3. Или используя docker-compose
-docker-compose up -d
+# Запуск
+docker run -p 8000:8000 dxa-quality-api
```
+---
-📡 API Endpoints
+## API Endpoints
-| Метод | Эндпоинт | Описание |
-|--------|-------------------|--------------------------|
-| GET | / | Главная страница
-| GET | /docs | Swagger UI документация
-| GET | /redoc | ReDoc документация
-| GET | /api/v1/health | Проверка статуса сервиса
-| POST | /api/v1/analyze | Базовый анализ изображения
-| POST | /api/v1/analyze/detailed | Детальный анализ с метриками
-| POST | /api/v1/analyze/sr | Анализ с DICOM SR отчётом
-| POST | /api/v1/batch | Пакетный анализ файлов
-| POST | /api/v1/export | Анализ и экспорт в XLSX
+| Метод | Эндпоинт | Описание |
+|-------|----------|----------|
+| GET | `/` | Главная страница (веб-интерфейс) |
+| GET | `/api/v1/health` | Проверка статуса сервиса |
+| POST | `/api/v1/analyze` | Базовый анализ изображения |
+| POST | `/api/v1/analyze/detailed` | Детальный анализ с метриками |
+| POST | `/api/v1/analyze/sr` | DICOM SR отчёт |
+| POST | `/api/v1/batch` | Пакетный анализ |
+| POST | `/api/v1/export` | Анализ и экспорт в XLSX |
+### Пример использования
-Пример запроса
```bash
+# Анализ файла
curl -X POST "http://localhost:8000/api/v1/analyze" \
-H "accept: application/json" \
-H "Content-Type: multipart/form-data" \
- -F "file=@/path/to/image.jpg"
+ -F "file=@/path/to/image.dcm"
+
+# Детальный анализ
+curl -X POST "http://localhost:8000/api/v1/analyze/detailed" \
+ -H "accept: application/json" \
+ -H "Content-Type: multipart/form-data" \
+ -F "file=@/path/to/image.dcm"
```
-Пример ответа
-
-```json
-{
- "overall_quality": "GOOD",
- "severity": "LOW",
- "issues": [],
- "confidence": 0.9,
- "metrics": {
- "artifact": {
- "artifact": false,
- "num_objects": 1,
- "edge_energy": 0.234,
- "object_size": 0.123
- },
- "position": {
- "position": [0.45, 0.52],
- "valid": true,
- "deviation": 0.032
- },
- "contrast": {
- "valid": true,
- "contrast": 0.456
- }
- },
- "mask": "base64_encoded_mask_image"
-}
-```
-
-### Детальный анализ (/api/v1/analyze/detailed)
-
-Возвращает расширенную информацию с детализацией нарушений:
+### Ответ детального анализа
```json
{
@@ -156,7 +166,7 @@ curl -X POST "http://localhost:8000/api/v1/analyze" \
"metrics": {
"motion": {
"motion_detected": true,
- "blur_laplacian": 0.001,
+ "blur_laplacian": 0.0008,
"severity": "HIGH"
},
"artifacts": {
@@ -167,147 +177,153 @@ curl -X POST "http://localhost:8000/api/v1/analyze" \
"valid": true
}
},
- "spine_completeness": {
- "valid": true,
- "num_vertebrae": 4
- },
"overall_quality": "POOR",
"severity": "HIGH"
}
```
+---
-📊 Интерфейс
-Веб-интерфейс доступен по адресу http://localhost:8000/:
+## Структура проекта
- - 📤 Drag-and-drop загрузка изображений
- - 🔍 Автоматический анализ
- - 🎯 Визуализация маски сегментации
- - 📈 Детальные метрики качества
- - 🏷️ Подробный отчет о нарушениях
- - 👆 Клик по строке для просмотра деталей
-
-
-🛠️ Технологии
-
-|Компонент | Технология
-|-|-|
-|Бэкенд | Python 3.10, FastAPI, Uvicorn
-|ML | PyTorch, NumPy, SciPy
-|Обработка изображений | PIL, OpenCV
-|Визуализация | HTML5, CSS3, Canvas API
-|Контейнеризация | Docker, Docker Compose
-|Документация | Swagger UI, ReDoc
-
-
-📁 Структура проекта
-```text
+```
bone_2026/
├── src/
-│ ├── dxa/ # DXA Quality модуль
-│ │ ├── model.py # ResNet18 классификатор
-│ │ ├── dataset.py # Загрузчик данных
-│ │ ├── train.py # Обучение
-│ │ └── inference.py # Инференс
-│ ├── api/ # REST API
-│ ├── quality/ # Оценка качества
-│ └── main.py # FastAPI приложение
-├── models/
-│ └── dxa_model.pth # Обученная модель
-├── dataset_hack/ # DICOM датасет
+│ ├── main.py # FastAPI приложение
+│ ├── run.py # Запуск сервера
+│ ├── dxa/ # DXA модуль
+│ │ ├── model.py # ResNet18 классификатор
+│ │ ├── dataset.py # Загрузчик данных
+│ │ ├── train.py # Обучение модели
+│ │ └── inference.py # Инференс и batch-обработка
+│ ├── quality/ # Оценка качества
+│ │ ├── quality_scorer.py # Базовый скорer
+│ │ └── detailed_assessment.py # Детальный анализ
+│ ├── api/ # REST API
+│ │ ├── endpoints.py # Дополнительные эндпоинты
+│ │ └── static/ # Веб-интерфейс
+│ └── utils/ # Утилиты
+├── models/ # Обученные модели
+│ └── dxa_model.pth # Модель классификатора
+├── dataset_hack/ # Датасет для обучения/тестирования
+├── docs/ # Документация
├── Dockerfile
├── requirements.txt
-├── run.sh
└── README.md
```
-🧪 Тестирование
-```bash
-# Запуск тестов (если есть)
-pytest tests/
-```
+---
-# Проверка API
+## Обучение модели
+
+### Подготовка данных
+
+1. Разместите DICOM-файлы в `dataset_hack/НД_для_обучения/Исследования/`
+2. Подготовьте Excel-файл разметки `dataset_hack/НД_для_обучения/разметка.xlsx`
+
+Столбцы разметки:
+- `study_uid` — ID исследования
+- `позвоночник_укладка`, `позвоночник_ось`, `позвоночник_артефакты` — критерии для позвоночника
+- `бедро_позиция_лев`, `бедро_roi_лев` — критерии для левого бедра
+- `бедро_позиция_прав`, `бедро_roi_прав` — критерии для правого бедра
+- `итог_позвоночник`, `итог_бедро_лев`, `итог_бедро_прав` — итоговая оценка (0/1)
+
+### Запуск обучения
```bash
-curl http://localhost:8000/api/v1/health
+python src/dxa/train.py \
+ --epochs 20 \
+ --batch-size 8 \
+ --backbone resnet18 \
+ --output-dir models
```
-## 📊 Метрики качества
+### Аргументы
-Артефакты
+| Параметр | По умолчанию | Описание |
+|----------|-------------|----------|
+| `--data-root` | `dataset_hack` | Путь к директории с данными |
+| `--annotation-path` | `dataset_hack/НД_для_обучения/разметка.xlsx` | Путь к файлу разметки |
+| `--epochs` | 20 | Количество эпох |
+| `--batch-size` | 8 | Размер батча |
+| `--backbone` | `resnet18` | Архитектура (resnet18/resnet34/efficientnet_b0) |
+| `--input-size` | 224 | Размер входного изображения |
+| `--output-dir` | `models` | Директория для сохранения модели |
-*Описание:* Обнаружение шума, размытости, фрагментации
+---
-Проверка:
+## Инференс
- - Энергия границ (edge_energy)
- - Количество объектов (num_objects)
- - Размер объекта (object_size)
+### Одиночный файл
-### Позиционирование
-
-*Описание:* Проверка правильности расположения объекта
-
-Проверка:
-
- - Центр масс объекта
- - Отклонение от центра изображения
- - Минимальный размер объекта
-
-### Контраст
-
-*Описание:* Оценка качества изображения
-
-Проверка:
-
- - Контраст внутри объекта
- - Отношение средних (объект/фон)
- - Интенсивность пикселей
-
-## 🚀 Деплой
-На сервер
```bash
-# Копирование на сервер
-scp -r ./bone-quality-assessment user@server:/var/www/
+python src/dxa/inference.py \
+ --input-path path/to/image.dcm \
+ --output-path result.xlsx
```
-# Запуск в фоновом режиме
-nohup python run.py > logs/out.log 2>&1 &
+### Директория
-Использование с Nginx
-```nginx
-location /api/ {
- proxy_pass http://localhost:8000;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
-}
+```bash
+python src/dxa/inference.py \
+ --input-path dataset_hack/Для\ теста \
+ --output-path results.xlsx \
+ --model-path models/dxa_model.pth
```
-## 🤝 Вклад в проект
- - Fork репозитория
- - Создайте ветку для вашей фичи (git checkout -b feature/amazing-feature)
- - Commit изменений (git commit -m 'Add amazing feature')
- - Push в ветку (git push origin feature/amazing-feature)
- - Откройте Pull Request
+### Выходной формат (XLSX/CSV)
+| Колонка | Описание |
+|---------|----------|
+| `path_to_study` | Путь к директории исследования |
+| `study_uid` | StudyInstanceUID |
+| `image_uid` | SOPInstanceUID |
+| `anatomical_region` | Анатомическая область (spine/hip_left/hip_right) |
+| `quality_class` | Класс качества (0 — OK, 1 — Violation) |
+| `violation_type` | Тип нарушения |
+| `processing_status` | Статус обработки |
+| `time_of_processing` | Время обработки (сек) |
-**📄 Лицензия**
+---
+
+## Метрики качества
+
+### Детекция движения
+
+- **Laplacian variance** — дисперсия лапласиана (меньше = сильнее размытие)
+- **FFT high-frequency ratio** — отношение высокочастотной энергии (меньше = размытие)
+- **Edge duplication** — проверка "призрачных" контуров
+
+### Детекция артефактов
+
+- **Metal detection** — яркие области (>99.5 перцентиль)
+- **Implant detection** — линейные структуры (морфологические операции)
+- **Cement detection** — локальные яркие пятна в ROI
+- **Calcification** — малые яркие области вне ROI
+
+### Полнота изображения
+
+- **Spine**: подсчет позвонков (ожидается 3-4), проверка межпозвоночных промежутков
+- **Hip**: проверка видимости шейки бедра, большого/малого вертелов
+
+### Валидация ROI
+
+- Проверка отступа от краев (>10 пикселей)
+- Проверка размера ROI (>30% высоты, >20% ширины изображения)
+
+---
+
+## Лицензия
MIT License
-👥 Команда
-Грачев Денис — Разработка - GitHub
+## Команда
-### 🙏 Благодарности
+- **Грачев Денис** — Разработка
+- **Грачев Татьяна** — Капитан
-***Oxford-IIIT Pet Dataset*** для обучения модели
+---
-***Сообществу PyTorch и FastAPI***
-
-📞 Контакты
- - 📧 Email: your.email@example.com
- - 🐦 Telegram: @oxydencher
- - 🐙 GitHub: gdg6
-
-
Built with ❤️ for the Bone Quality Assessment Hackathon
+
+ Built for Bone Quality Assessment Hackathon 2026
+
diff --git a/prediction_result.png b/prediction_result.png
deleted file mode 100644
index 1ba652c..0000000
Binary files a/prediction_result.png and /dev/null differ
diff --git a/requirements.txt b/requirements.txt
index bdf488d..98541cb 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -62,6 +62,7 @@ timm==1.0.29
torch==2.8.0
torchvision==0.23.0
TotalSegmentator==2.18.0
+requests==2.32.3
tqdm==4.70.0
typer==0.23.2
typing-inspection==0.4.2
diff --git a/src/core/orchestrator.py b/src/core/orchestrator.py
index cedc9ab..b523d59 100644
--- a/src/core/orchestrator.py
+++ b/src/core/orchestrator.py
@@ -7,16 +7,12 @@ from typing import Dict, Any
from src.classifiers.bone_condition_classifier import BoneConditionClassifier
from src.core.config import PipelineConfig
from src.core.registry import ModelRegistry
-from src.classifiers.pet_breed_classifier import PetBreedClassifier
from src.quality.universal_scorer import UniversalQualityScorer
from src.segmentators.totalsegmentator_wrapper import TotalSegmentatorWrapper
-from src.segmentators.unet_segmentator import UNetSegmentator
# Регистрируем модели
-ModelRegistry.register_segmentator("unet", UNetSegmentator)
ModelRegistry.register_segmentator("totalsegmentator", TotalSegmentatorWrapper)
-ModelRegistry.register_classifier("pet_breed", PetBreedClassifier)
ModelRegistry.register_classifier("bone_condition", BoneConditionClassifier)
@@ -43,7 +39,11 @@ class Orchestrator:
# Сегментатор
segmentator_class = ModelRegistry.get_segmentator(self.config.segmentator)
if segmentator_class:
- self.segmentator = segmentator_class(device=self.device)
+ # Передаем device для TotalSegmentator
+ if self.config.segmentator == "totalsegmentator":
+ self.segmentator = segmentator_class(device=self.device)
+ else:
+ self.segmentator = segmentator_class(device=self.device)
else:
raise ValueError(f"Сегментатор {self.config.segmentator} не найден")
diff --git a/src/dxa/inference.py b/src/dxa/inference.py
index 708c8c0..84464c7 100644
--- a/src/dxa/inference.py
+++ b/src/dxa/inference.py
@@ -1,6 +1,16 @@
#!/usr/bin/env python3
"""
DXA Quality Inference - Batch processing with Excel output
+
+Этот модуль выполняет инференс модели классификации качества DXA исследований.
+Основные функции:
+- Загрузка и предобработка DICOM изображений
+- Определение анатомической области (позвоночник/бедро)
+- Бинарная классификация качества (OK/Violation)
+- Пакетная обработка с экспортом в Excel
+
+Использование:
+ python src/dxa/inference.py --input-path --output-path
"""
import os
import sys
@@ -22,7 +32,15 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
def get_device():
- """Get best available device"""
+ """
+ Определение доступного устройства для вычислений.
+
+ Порядок приоритета: MPS (Apple Silicon) -> CUDA (NVIDIA GPU) -> CPU.
+ Это нужно для максимальной производительности на доступном железе.
+
+ Returns:
+ str: Устройство ('mps', 'cuda' или 'cpu')
+ """
if torch.backends.mps.is_available():
return 'mps'
elif torch.cuda.is_available():
@@ -32,9 +50,23 @@ def get_device():
def load_model(model_path: str, backbone: str = 'resnet18', device: str = 'cpu'):
- """Load trained model"""
- from src.dxa.model import create_model
+ """
+ Загрузка обученной модели классификатора качества DXA.
+ Модель использует предобученный ResNet18 в качестве backbone и
+ добавляет классификационную голову для бинарной классификации
+ (качество OK vs Violation).
+
+ Args:
+ model_path: Путь к файлу модели (.pth)
+ backbone: Архитектура backbone (resnet18/resnet34/efficientnet_b0)
+ device: Устройство для загрузки модели
+
+ Returns:
+ DXAQualityModel: Обертка модели с методами predict и load
+ """
+ from src.dxa.model import create_model
+
model = create_model(backbone=backbone, pretrained=False, device=device)
model.load(model_path)
model.model.eval()
@@ -42,33 +74,50 @@ def load_model(model_path: str, backbone: str = 'resnet18', device: str = 'cpu')
def load_dicom_image(dcm_path: str, input_size: int = 224) -> torch.Tensor:
- """Load and preprocess DICOM image"""
+ """
+ Загрузка и предобработка DICOM изображения для модели.
+
+ Этапы предобработки:
+ 1. Чтение DICOM и извлечение pixel_array
+ 2. Нормализация интенсивности в диапазон [0, 1]
+ 3. Преобразование в 3 канала (дублирование для RGB)
+ 4. Изменение размера до input_size x input_size
+ 5. Нормализация для ImageNet (деление на 255)
+ 6. Преобразование в PyTorch тензор
+
+ Args:
+ dcm_path: Путь к DICOM файлу
+ input_size: Целевой размер изображения (по умолчанию 224 для ResNet)
+
+ Returns:
+ torch.Tensor: Тензор изображения формы (1, 3, 224, 224)
+ """
ds = pydicom.dcmread(dcm_path)
img = ds.pixel_array.astype(np.float32)
-
+
# Normalize to 0-1
img_min = img.min()
img_max = img.max()
if img_max > img_min:
img = (img - img_min) / (img_max - img_min)
-
+
# Convert to 3-channel
img = np.stack([img] * 3, axis=0)
-
+
# Convert to uint8 for PIL
img = (img * 255).astype(np.uint8)
-
+
# Resize
img_pil = Image.fromarray(img.transpose(1, 2, 0))
img_pil = img_pil.resize((input_size, input_size), Image.BILINEAR)
img = np.array(img_pil).transpose(2, 0, 1)
-
+
# Normalize back to 0-1
img = img.astype(np.float32) / 255.0
-
+
# Convert to tensor
img = torch.from_numpy(img).float().unsqueeze(0)
-
+
return img
@@ -95,24 +144,30 @@ def extract_anatomical_region_from_filename(dcm_path: str) -> str:
def determine_region_from_image(img: np.ndarray) -> str:
"""
- Determine anatomical region from DICOM image content.
+ Определение анатомической области (позвоночник/бедро) по содержимому изображения.
- Uses multiple features:
- - Bright region aspect ratio (main discriminator for spine vs hip)
- - Image symmetry
- - Horizontal position for left/right hip distinction
+ Алгоритм использует анализ формы яркой области на изображении:
+ - Позвоночник: яркая область более квадратная (aspect ratio ~1.2)
+ - Бедро: яркая область вытянута вертикально (aspect ratio > 1.5)
- Returns: 'spine', 'hip_left', 'hip_right', or 'hip' (unknown side)
+ Дополнительно для определения левого/правого бедра:
+ - Сравнение яркости левой и правой половин изображения
+
+ Args:
+ img: Нормализованное изображение (np.array)
+
+ Returns:
+ str: 'spine', 'hip_left', 'hip_right' или 'hip' (неопределенная сторона)
"""
h, w = img.shape
-
+
# Normalize image
img_norm = (img - img.min()) / (img.max() - img.min() + 1e-6)
-
+
# Feature 1: Bright region aspect ratio
threshold = np.percentile(img_norm, 95)
binary = img_norm > threshold
-
+
bbox_aspect = 1.0
bright_x = 0.5 # default center
left_right_ratio = 1.0 # default balanced
@@ -127,36 +182,36 @@ def determine_region_from_image(img: np.ndarray) -> str:
bbox_h = rmax - rmin
bbox_w = cmax - cmin
bbox_aspect = bbox_h / (bbox_w + 1e-6)
-
+
# Get bright region center X position
com = ndimage.center_of_mass(binary)
bright_x = com[1] / w
-
+
# Calculate left/right brightness ratio
left_bright = binary[:, :w//2].sum()
right_bright = binary[:, w//2:].sum()
left_right_ratio = left_bright / (right_bright + 1e-6)
except:
pass
-
+
# Feature 2: Symmetry
h_mid, w_mid = h // 2, w // 2
left_half = img_norm[:, :w_mid]
right_half = np.fliplr(img_norm[:, w_mid:])
min_w = min(left_half.shape[1], right_half.shape[1])
symmetry = 1 - np.abs(left_half[:, :min_w] - right_half[:, :min_w]).mean() / (img_norm.std() + 1e-6)
-
+
# Feature 3: Vertical/horizontal edges
dx = np.diff(img_norm, axis=1)
dy = np.diff(img_norm, axis=0)
v_edges = np.abs(dx).mean()
h_edges = np.abs(dy).mean()
v_h_ratio = v_edges / (h_edges + 1e-6)
-
+
# Classification rules based on analysis:
# spine: bbox_aspect ~1.2, symmetry > 0.4, v_h_ratio < 1.5
# hip: bbox_aspect > 1.5, symmetry < 0.4, v_h_ratio > 1.5
-
+
# Primary: bbox_aspect is the best discriminator
if bbox_aspect < 1.5:
# More square bright region -> spine
@@ -215,12 +270,27 @@ def determine_anatomical_region(dcm_path: str) -> str:
def process_dicom_files(args):
- """Run inference on DICOM files"""
+ """
+ Основная функция пакетного инференса DXA изображений.
+ Этапы обработки:
+ 1. Определение устройства (MPS/CUDA/CPU)
+ 2. Загрузка обученной модели
+ 3. Поиск DICOM файлов в указанной директории
+ 4. Инференс для каждого файла:
+ - Предобработка изображения
+ - Предикт модели (бинарная классификация)
+ - Определение анатомической области
+ - Сохранение метаданных DICOM
+ 5. Экспорт результатов в Excel/CSV
+
+ Args:
+ args: Аргументы командной строки (input_path, output_path, model_path и т.д.)
+ """
# Setup
device = get_device()
print(f"Using device: {device}")
-
+
# Load model
if Path(args.model_path).exists():
print(f"Loading model from {args.model_path}...")
@@ -231,61 +301,61 @@ def process_dicom_files(args):
print("Using untrained model - results will be random")
from src.dxa.model import create_model
model = create_model(backbone=args.backbone, pretrained=False, device=device)
-
+
# Find DICOM files
input_path = Path(args.input_path)
dcm_files = []
-
+
if input_path.is_file() and input_path.suffix.lower() == '.dcm':
dcm_files = [input_path]
elif input_path.is_dir():
dcm_files = sorted(input_path.rglob('*.dcm'))
-
+
print(f"Found {len(dcm_files)} DICOM files")
-
+
if len(dcm_files) == 0:
print("ERROR: No DICOM files found!")
return
-
+
# Process each file
results = []
-
+
for dcm_path in tqdm(dcm_files, desc="Processing"):
start_time = datetime.now()
-
+
try:
# Load and preprocess image
img = load_dicom_image(str(dcm_path), args.input_size)
img = img.to(device)
-
+
# Predict
with torch.no_grad():
outputs = model.model(img)
probs = torch.softmax(outputs, dim=1)
pred = outputs.argmax(dim=1).item()
prob = probs[0, pred].item()
-
+
# Get DICOM metadata
ds = pydicom.dcmread(str(dcm_path))
-
+
study_uid = getattr(ds, 'StudyInstanceUID', '')
image_uid = getattr(ds, 'SOPInstanceUID', '')
-
+
# Determine anatomical region
anatomical_region = determine_anatomical_region(str(dcm_path))
-
+
# Map prediction to quality class
quality_class = pred # 0 = good, 1 = violation
-
+
# Determine violation type (simplified)
if quality_class == 0:
violation_type = ''
else:
# In real implementation, this would come from a more detailed model
violation_type = 'quality_violation_detected'
-
+
processing_time = (datetime.now() - start_time).total_seconds()
-
+
results.append({
'path_to_study': str(dcm_path.parent),
'study_uid': study_uid,
@@ -297,7 +367,7 @@ def process_dicom_files(args):
'time_of_processing': processing_time,
'confidence': round(prob, 4)
})
-
+
except Exception as e:
processing_time = (datetime.now() - start_time).total_seconds()
results.append({
diff --git a/src/dxa/train.py b/src/dxa/train.py
index 76042ea..e84ef7c 100644
--- a/src/dxa/train.py
+++ b/src/dxa/train.py
@@ -1,6 +1,20 @@
#!/usr/bin/env python3
"""
Training script for DXA Quality Classifier
+=========================================
+
+Скрипт обучения модели классификации качества DXA изображений.
+
+Основные этапы:
+1. Загрузка данных из Excel разметки
+2. Создание DataLoader с разбиением на train/val
+3. Инициализация модели (ResNet18 с предобучением на ImageNet)
+4. Обучение с использованием CrossEntropyLoss
+5. Валидация после каждой эпохи
+6. Сохранение лучшей модели по F1-score
+
+Использование:
+ python src/dxa/train.py --epochs 20 --batch-size 8
"""
import os
import sys
@@ -31,24 +45,39 @@ def get_device():
def compute_metrics(preds, labels):
- """Compute classification metrics"""
+ """
+ Вычисление метрик классификации.
+
+ Рассчитывает:
+ - Accuracy: доля правильных предсказаний
+ - Precision: точность (доля TP среди предсказанных positive)
+ - Recall: полнота (доля TP среди реальных positive)
+ - F1: гармоническое среднее precision и recall
+
+ Args:
+ preds: Предсказания модели (numpy array)
+ labels: Истинные метки (numpy array)
+
+ Returns:
+ Dict с метриками
+ """
preds = np.array(preds)
labels = np.array(labels)
-
+
# Accuracy
accuracy = (preds == labels).mean()
-
+
# True/False positives/negatives
tp = ((preds == 1) & (labels == 1)).sum()
tn = ((preds == 0) & (labels == 0)).sum()
fp = ((preds == 1) & (labels == 0)).sum()
fn = ((preds == 0) & (labels == 1)).sum()
-
+
# Precision, Recall, F1
precision = tp / (tp + fp) if (tp + fp) > 0 else 0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
-
+
return {
'accuracy': accuracy,
'precision': precision,
@@ -62,7 +91,24 @@ def compute_metrics(preds, labels):
def train(args):
- """Main training loop"""
+ """
+ Основной цикл обучения модели.
+
+ Этапы:
+ 1. Определение устройства (MPS/CUDA/CPU)
+ 2. Создание директории для сохранения модели
+ 3. Загрузка данных (DataLoader)
+ 4. Создание модели
+ 5. Цикл обучения по эпохам:
+ - Обучение на train set
+ - Валидация на val set
+ - Расчет метрик (accuracy, precision, recall, F1)
+ - Сохранение лучшей модели по F1
+ 6. Сохранение финальной модели
+
+ Args:
+ args: Аргументы командной строки
+ """
# Setup
device = get_device()
diff --git a/src/main.py b/src/main.py
index ba9103d..bc67e33 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1,5 +1,24 @@
"""
Main FastAPI application for DXA Quality Assessment
+=================================================
+
+FastAPI сервер для оценки качества DXA исследований.
+
+Основные функции:
+- REST API для анализа DICOM изображений
+- Веб-интерфейс для загрузки и визуализации
+- Поддержка одиночного и пакетного анализа
+- Экспорт результатов в XLSX
+- Генерация DICOM SR (Structured Report)
+
+Эндпоинты:
+- GET / - Главная страница (веб-интерфейс)
+- GET /api/v1/health - Проверка статуса
+- POST /api/v1/analyze - Базовый анализ
+- POST /api/v1/analyze/detailed - Детальный анализ
+- POST /api/v1/analyze/sr - DICOM SR отчет
+- POST /api/v1/batch - Пакетный анализ
+- POST /api/v1/export - Анализ и экспорт в XLSX
"""
from fastapi import FastAPI, File, UploadFile, APIRouter, Query
from fastapi.staticfiles import StaticFiles
@@ -43,13 +62,21 @@ device = None
def load_model():
- """Load DXA model"""
- global dxa_model, device
+ """
+ Загрузка модели классификатора качества DXA.
+ Модель загружается глобально при первом запросе и сохраняется в памяти.
+ Это позволяет избежать повторной загрузки при каждом запросе.
+
+ Returns:
+ DXAQualityModel: Обученная модель или None при ошибке
+ """
+ global dxa_model, device
+
if dxa_model is None:
device = get_device()
print(f"Loading DXA model on {device}...")
-
+
try:
dxa_model = create_model(
backbone='resnet18',
@@ -62,37 +89,55 @@ def load_model():
except Exception as e:
print(f"Error loading model: {e}")
dxa_model = None
-
+
return dxa_model
def preprocess_dicom(dcm_bytes: bytes, input_size: int = 224):
- """Preprocess DICOM for model input"""
- import tempfile
+ """
+ Предобработка DICOM изображения для модели.
+ Этапы:
+ 1. Сохранение байтов во временный файл
+ 2. Чтение DICOM (pydicom)
+ 3. Нормализация значений пикселей
+ 4. Создание 3-канального изображения
+ 5. Изменение размера до input_size x input_size
+ 6. Нормализация для PyTorch (деление на 255)
+ 7. Преобразование в тензор
+
+ Args:
+ dcm_bytes: Байты DICOM файла
+ input_size: Целевой размер (по умолчанию 224)
+
+ Returns:
+ Tuple[torch.Tensor, pydicom.Dataset]: Тензор изображения и метаданные DICOM
+ """
+ import tempfile
+
with tempfile.NamedTemporaryFile(suffix='.dcm', delete=False) as f:
f.write(dcm_bytes)
dcm_path = f.name
-
+
ds = pydicom.dcmread(dcm_path)
img = ds.pixel_array.astype(np.float32)
-
+
# Normalize
img = (img - img.min()) / (img.max() - img.min() + 1e-8)
-
+
# 3-channel
img = np.stack([img] * 3, axis=0)
-
+
# Resize
img = (img * 255).astype(np.uint8)
img_pil = Image.fromarray(img.transpose(1, 2, 0))
img_pil = img_pil.resize((input_size, input_size), Image.BILINEAR)
img = np.array(img_pil).transpose(2, 0, 1)
img = img.astype(np.float32) / 255.0
-
+
# Tensor
img = torch.from_numpy(img).float().unsqueeze(0)
-
+
return img, ds
@@ -192,14 +237,18 @@ async def analyze_dicom_detailed(
include_visualization: bool = Query(False, description="Include base64 mask visualization")
):
"""
- Detailed analysis of DICOM file with comprehensive quality assessment.
+ Детальный анализ DICOM файла с комплексной оценкой качества.
+
+ Это основной эндпоинт для получения полной информации о качестве исследования.
+ В отличие от базового /analyze, возвращает:
+ - violation_type: конкретный тип нарушения
+ - reason: человекочитаемое объяснение
+ - confidence_per_class: вероятности для каждого класса
+ - view_quality: полный/частичный вид
+ - metrics: детальные метрики по каждому критерию
Returns:
- - violation_type: Specific type of violation (correct, position_error, artifact_motion, etc.)
- - reason: Human-readable explanation of why the image is non-compliant
- - confidence_per_class: Probability for each class
- - view_quality: Full or partial view assessment
- - Detailed metrics for motion, artifacts, position, ROI
+ JSON с детальным отчетом о качестве исследования
"""
try:
# Load model
@@ -484,7 +533,26 @@ async def analyze_dicom_sr(file: UploadFile = File(...)):
def generate_dicom_sr_text(study_uid: str, image_uid: str, quality_report: Dict) -> str:
- """Generate DICOM SR text representation"""
+ """
+ Генерация текстового представления DICOM SR (Structured Report).
+
+ DICOM SR - это стандартизированный формат отчета в DICOM.
+ Здесь генерируется текстовое представление для совместимости.
+
+ Структура отчета:
+ - Study/Series Instance UID
+ - Procedure Report (регион, качество, уверенность)
+ - Findings (тип нарушения, причина, метрики)
+ - Completion Flag
+
+ Args:
+ study_uid: StudyInstanceUID из DICOM
+ image_uid: SOPInstanceUID из DICOM
+ quality_report: Результат generate_quality_report()
+
+ Returns:
+ str: Текстовое представление SR отчета
+ """
# Map violation types to DICOM codes (simplified)
violation_code_map = {
diff --git a/src/quality/detailed_assessment.py b/src/quality/detailed_assessment.py
index 2dc9c5d..1f4cad7 100644
--- a/src/quality/detailed_assessment.py
+++ b/src/quality/detailed_assessment.py
@@ -1,6 +1,28 @@
"""
Detailed Quality Assessment Module for DXA images
-Implements checks from condition.txt and condition_doctor.txt
+================================================
+
+Модуль детальной оценки качества DXA изображений.
+Реализует проверки по критериям из condition.txt и condition_doctor.txt.
+
+Основные функции:
+- detect_motion_blur(): Детекция артефактов движения
+- detect_artifacts(): Детекция металлических объектов, имплантатов, цемента
+- check_spine_completeness(): Проверка видимости позвонков L1-L4
+- check_hip_completeness(): Проверка видимости бедра
+- check_hip_rotation(): Проверка угла ротации бедра
+- check_roi_boundaries(): Валидация границ ROI
+- generate_quality_report(): Генерация итогового отчета
+
+Типы нарушений (violation_types):
+- correct: Качество соответствует норме
+- artifact_motion: Артефакт движения
+- artifact_other: Прочие артефакты (металл, имплантаты)
+- position_error: Ошибка позиционирования
+- rotation: Нарушение ротации
+- incomplete_view: Неполный вид
+- roi_error: Ошибка ROI
+- labeling_error: Ошибка разметки
"""
import numpy as np
from scipy import ndimage
@@ -82,37 +104,54 @@ def calculate_blur_fft(image: np.ndarray) -> float:
def detect_motion_blur(image: np.ndarray, segmentation: Optional[np.ndarray] = None) -> Dict[str, Any]:
"""
- Detect motion artifacts in the image.
+ Детекция артефактов движения на DXA изображении.
- Checks:
- - Blur using Laplacian variance
- - Blur using FFT high-frequency energy
- - Edge duplication (ghost edges)
+ Артефакты движения проявляются в виде:
+ 1. Размытия (blur) - снижение четкости границ
+ 2. Дублирования краев (edge duplication) - "призрачные" контуры
+
+ Методы детекции:
+ - Laplacian variance: дисперсия лапласиана изображения
+ (низкое значение = сильное размытие)
+ - FFT high-frequency energy: отношение высокочастотной энергии
+ (низкое значение = размытие)
+ - Edge duplication: проверка повторяющихся границ
+
+ Args:
+ image: Исходное изображение (нормализованное, 0-1)
+ segmentation: Бинарная маска сегментации (опционально)
+
+ Returns:
+ Dict с ключами:
+ - motion_detected: bool - обнаружено ли движение
+ - blur_laplacian: float - дисперсия лапласиана
+ - blur_fft: float - FFT high-frequency ratio
+ - severity: str - степень (NONE/MEDIUM/HIGH)
"""
if len(image.shape) == 3:
gray = np.mean(image, axis=2)
else:
gray = image
-
+
# Normalize
gray = (gray - gray.min()) / (gray.max() - gray.min() + 1e-8)
-
+
# Calculate blur metrics
laplacian_var = calculate_blur_laplacian(gray)
fft_blur = calculate_blur_fft(gray)
-
+
# Thresholds (tuned for medical images)
# Lower laplacian variance = more blur
is_blurred_laplacian = laplacian_var < 0.002
is_blurred_fft = fft_blur < 0.3
-
+
# Check for edge duplication (ghost edges)
edge_duplication = False
if segmentation is not None and segmentation.sum() > 0:
# Find edges in segmentation
edges = ndimage.sobel(segmentation.astype(float))
edge_positions = np.where(np.abs(edges) > 0)
-
+
if len(edge_positions[0]) > 10:
# Check if there are duplicate edges (offset)
y_coords = edge_positions[0]
@@ -120,9 +159,9 @@ def detect_motion_blur(image: np.ndarray, segmentation: Optional[np.ndarray] = N
# If there are many small gaps, might be edge duplication
small_gaps = np.sum(y_diff < 3)
edge_duplication = small_gaps > len(y_coords) * 0.1
-
+
motion_detected = is_blurred_laplacian or is_blurred_fft
-
+
return {
"motion_detected": motion_detected,
"blur_laplacian": laplacian_var,
@@ -136,20 +175,34 @@ def detect_motion_blur(image: np.ndarray, segmentation: Optional[np.ndarray] = N
def detect_artifacts(image: np.ndarray, segmentation: Optional[np.ndarray] = None) -> Dict[str, Any]:
"""
- Detect various artifacts in the image:
- - Metal objects (high intensity regions)
- - Implants, screws, plates
- - Cement
- - Calcifications
+ Детекция артефактов на DXA изображении.
+
+ Типы артефактов:
+ - Metal (металл): очень яркие области (>99.5 перцентиль), небольшого размера
+ - Implants (имплантаты): регулярные геометрические структуры (пластины, винты)
+ - Cement (цемент): локальные яркие пятна в области ROI
+ - Calcifications (кальцификаты): малые яркие области вне ROI
+
+ Args:
+ image: Исходное изображение (нормализованное)
+ segmentation: Бинарная маска сегментации (опционально)
+
+ Returns:
+ Dict с ключами:
+ - metal_detected: bool
+ - implant_detected: bool
+ - cement_detected: bool
+ - calcification_detected: bool
+ - any_detected: bool - любой артефакт обнаружен
"""
if len(image.shape) == 3:
gray = np.mean(image, axis=2)
else:
gray = image
-
+
# Normalize
gray = (gray - gray.min()) / (gray.max() - gray.min() + 1e-8)
-
+
artifacts = {
"metal_detected": False,
"implant_detected": False,
@@ -157,29 +210,29 @@ def detect_artifacts(image: np.ndarray, segmentation: Optional[np.ndarray] = Non
"calcification_detected": False,
"local_defects": []
}
-
+
# Metal detection: very bright spots
metal_threshold = np.percentile(gray, 99.5)
metal_mask = gray > metal_threshold
metal_ratio = metal_mask.sum() / gray.size
-
+
# If very small bright spots = possible metal
if metal_ratio > 0.001 and metal_ratio < 0.05:
artifacts["metal_detected"] = True
-
+
# Implant detection: check for regular geometric shapes
if segmentation is not None:
# Look for high-intensity linear structures
lines = ndimage.generate_binary_structure(2, 2)
bright_mask = gray > np.percentile(gray, 95)
-
+
# Check for linear structures (potential plates/screws)
opened = ndimage.binary_opening(bright_mask, structure=lines)
linear_structures = opened & ~ndimage.binary_erosion(bright_mask)
-
+
if linear_structures.sum() > 50:
artifacts["implant_detected"] = True
-
+
# Cement detection: localized bright patches
if segmentation is not None:
roi = gray * segmentation
@@ -188,7 +241,7 @@ def detect_artifacts(image: np.ndarray, segmentation: Optional[np.ndarray] = Non
cement_regions = roi_normalized > 0.9
if cement_regions.sum() > 100 and cement_regions.sum() < 5000:
artifacts["cement_detected"] = True
-
+
# Calcifications: small bright spots
if segmentation is not None:
outside_roi = (1 - segmentation) > 0
@@ -198,7 +251,7 @@ def detect_artifacts(image: np.ndarray, segmentation: Optional[np.ndarray] = Non
calcifications = (gray > bg_threshold) & (gray < np.percentile(gray, 99))
if 10 < calcifications.sum() < 500:
artifacts["calcification_detected"] = True
-
+
# Determine if any artifact detected
artifacts["any_detected"] = any([
artifacts["metal_detected"],
@@ -206,18 +259,34 @@ def detect_artifacts(image: np.ndarray, segmentation: Optional[np.ndarray] = Non
artifacts["cement_detected"],
artifacts["calcification_detected"]
])
-
+
return artifacts
def check_spine_completeness(segmentation: np.ndarray) -> Dict[str, Any]:
"""
- Check if spine vertebrae are fully visible (L1-L4).
+ Проверка полноты изображения позвоночника.
- Verifies:
- - Number of vertebrae (should be 3-4 for lumbar spine)
- - Vertebrae are not cropped
- - Intervertebral spaces visible
+ Для качественного исследования поясничного отдела позвоночника должны быть видны:
+ - 3-4 позвонка (L1-L4)
+ - Межпозвоночные промежутки
+ - Позвонки не должны быть обрезаны краями изображения
+
+ Алгоритм:
+ 1. Выделяет связные компоненты (отдельные позвонки)
+ 2. Проверяет количество (3-4)
+ 3. Проверяет каждый позвонок на обрезанность
+ 4. Проверяет выравнивание позвонков
+
+ Args:
+ segmentation: Бинарная маска сегментации позвоночника
+
+ Returns:
+ Dict с ключами:
+ - valid: bool - изображение полное
+ - num_vertebrae: int - количество видимых позвонков
+ - vertebrae: list - информация о каждом позвонке
+ - issues: list - список проблем
"""
if segmentation is None or segmentation.sum() == 0:
return {
@@ -226,38 +295,38 @@ def check_spine_completeness(segmentation: np.ndarray) -> Dict[str, Any]:
"num_vertebrae": 0,
"issues": ["Сегментация пустая"]
}
-
+
# Label connected components
labeled, num_features = ndimage.label(segmentation)
num_features = int(num_features)
-
+
issues = []
-
+
# Check number of vertebrae
if num_features < 3:
issues.append(f"Видимо слишком мало позвонков: {num_features} (ожидается 3-4)")
elif num_features > 6:
issues.append(f"Видимо слишком много объектов: {num_features}")
-
+
# Check each vertebra
vertebra_info = []
for i in range(1, num_features + 1):
vertebra_mask = labeled == i
y_coords, x_coords = np.where(vertebra_mask)
-
+
if len(y_coords) > 0:
# Get bounding box
y_min, y_max = y_coords.min(), y_coords.max()
x_min, x_max = x_coords.min(), x_coords.max()
-
+
height = y_max - y_min
width = x_max - x_min
-
+
# Check if cropped (touching image border)
h, w = segmentation.shape
- is_cropped = (y_min == 0 or y_max == h - 1 or
+ is_cropped = (y_min == 0 or y_max == h - 1 or
x_min == 0 or x_max == w - 1)
-
+
vertebra_info.append({
"id": i,
"height": int(height),
@@ -265,18 +334,18 @@ def check_spine_completeness(segmentation: np.ndarray) -> Dict[str, Any]:
"center": (float(np.mean(y_coords) / h), float(np.mean(x_coords) / w)),
"is_cropped": is_cropped
})
-
+
if is_cropped:
issues.append(f"Позвонок {i} обрезан (касается края изображения)")
-
+
# Check alignment
if len(vertebra_info) >= 2:
x_centers = [v["center"][1] for v in vertebra_info]
x_std = np.std(x_centers)
-
+
if x_std > 0.05: # More than 5% of width
issues.append(f"Позвонки не выровнены (отклонение {x_std:.3f})")
-
+
return {
"valid": len(issues) == 0,
"num_vertebrae": num_features,
@@ -303,10 +372,27 @@ def check_spine_labels(segmentation: np.ndarray, image: Optional[np.ndarray] = N
def check_hip_completeness(segmentation: np.ndarray) -> Dict[str, Any]:
"""
- Check if hip is fully visible:
- - Femur neck visible
- - Femur head visible
- - Greater and lesser trochanters visible
+ Проверка полноты изображения бедра.
+
+ Для качественного исследования бедра должны быть видны:
+ - Шейка бедра (femur neck)
+ - Головка бедра (femur head)
+ - Большой и малый вертелы
+
+ Алгоритм:
+ 1. Проверяет, что сегментация не пустая
+ 2. Проверяет обрезанность (касание краев изображения)
+ 3. Проверяет соотношение сторон (для бедра должно быть >1.5)
+
+ Args:
+ segmentation: Бинарная маска сегментации бедра
+
+ Returns:
+ Dict с ключами:
+ - valid: bool - изображение полное
+ - is_cropped: bool - обрезано ли изображение
+ - aspect_ratio: float - соотношение высоты к ширине
+ - issues: list - список проблем
"""
if segmentation is None or segmentation.sum() == 0:
return {
@@ -314,28 +400,28 @@ def check_hip_completeness(segmentation: np.ndarray) -> Dict[str, Any]:
"reason": "empty_segmentation",
"issues": ["Сегментация пустая"]
}
-
+
# Get bounding box
y_coords, x_coords = np.where(segmentation > 0)
h, w = segmentation.shape
-
- is_cropped = (y_coords.min() == 0 or y_coords.max() == h - 1 or
+
+ is_cropped = (y_coords.min() == 0 or y_coords.max() == h - 1 or
x_coords.min() == 0 or x_coords.max() == w - 1)
-
+
issues = []
-
+
# Check if cropped
if is_cropped:
issues.append("Бедро обрезано (касается края изображения)")
-
+
# Check aspect ratio (elongated = good for hip)
height = y_coords.max() - y_coords.min()
width = x_coords.max() - x_coords.min()
aspect_ratio = height / (width + 1e-8)
-
+
if aspect_ratio < 1.5:
issues.append(f"Слишком короткое изображение бедра (соотношение сторон: {aspect_ratio:.2f})")
-
+
return {
"valid": len(issues) == 0,
"is_cropped": is_cropped,
@@ -349,12 +435,27 @@ def check_hip_completeness(segmentation: np.ndarray) -> Dict[str, Any]:
def check_hip_rotation(segmentation: np.ndarray, image: np.ndarray) -> Dict[str, Any]:
"""
- Check hip rotation from the image.
+ Проверка ротации бедра на изображении.
- Proper positioning:
- - Femur should be slightly internally rotated (15-20 degrees)
- - Lesser trochanter should be slightly visible medially
- - Greater trochanter should not be prominent
+ Правильное позиционирование бедра:
+ - Легкая внутренняя ротация (15-20 градусов)
+ - Малый вертел слегка виден медиально
+ - Большой вертел не должен быть выражен
+
+ Алгоритм:
+ 1. Находит главную ость сегментированного бедра
+ 2. Вычисляет угол отклонения от вертикали
+ 3. Проверяет, что угол в пределах допустимого диапазона (±30°)
+
+ Args:
+ segmentation: Бинарная маска сегментации бедра
+ image: Исходное изображение
+
+ Returns:
+ Dict с ключами:
+ - valid: bool - ротация корректна
+ - rotation_angle: float - угол ротации в градусах
+ - issues: list - список проблем
"""
if segmentation is None or segmentation.sum() == 0:
return {
@@ -363,19 +464,19 @@ def check_hip_rotation(segmentation: np.ndarray, image: np.ndarray) -> Dict[str,
"rotation_angle": 0,
"issues": ["Нет сегментации"]
}
-
+
if len(image.shape) == 3:
gray = np.mean(image, axis=2)
else:
gray = image
-
+
# Get the femur shape
labeled, num_features = ndimage.label(segmentation)
-
+
issues = []
rotation_angle = 0
valid = True
-
+
if num_features == 0:
return {
"valid": False,
@@ -383,7 +484,7 @@ def check_hip_rotation(segmentation: np.ndarray, image: np.ndarray) -> Dict[str,
"rotation_angle": 0,
"issues": ["Сегментация пустая"]
}
-
+
# For a single femur, check orientation
if num_features == 1:
# Get major axis
@@ -392,23 +493,23 @@ def check_hip_rotation(segmentation: np.ndarray, image: np.ndarray) -> Dict[str,
# Compute covariance to get principal axis
coords_float = coords.astype(float)
cov = np.cov(coords_float[:, 0], coords_float[:, 1])
-
+
# Eigenvalue decomposition
eigenvalues, eigenvectors = np.linalg.eig(cov)
major_axis = eigenvectors[:, np.argmax(eigenvalues)]
-
+
# Calculate angle from vertical
angle = np.arctan2(major_axis[1], major_axis[0])
rotation_angle = np.degrees(angle)
-
+
# Check if within acceptable range (-30 to +30 degrees from vertical)
if abs(rotation_angle) > 30:
issues.append(f"Выраженная ротация: {rotation_angle:.1f}°")
valid = False
-
+
# Check lesser trochanter visibility (would be a bump on medial side)
# This is a simplified check
-
+
return {
"valid": valid,
"rotation_angle": float(rotation_angle),
@@ -634,17 +735,42 @@ def generate_quality_report(
model_confidence: float = 0.5
) -> Dict[str, Any]:
"""
- Generate comprehensive quality assessment report.
+ Генерация комплексного отчета о качестве исследования.
+
+ Этапы формирования отчета:
+ 1. Детекция движения (Laplacian, FFT)
+ 2. Детекция артефактов (металл, имплантаты, цемент)
+ 3. Проверка полноты (spine/hip)
+ 4. Проверка ротации (только для hip)
+ 5. Валидация ROI
+ 6. Определение типа нарушения
+ 7. Формирование итогового вердикта
+
+ Args:
+ image: Исходное изображение (нормализованное)
+ segmentation: Бинарная маска сегментации
+ region: Анатомическая область (spine/hip_left/hip_right)
+ model_prediction: Предсказание модели (0=OK, 1=Violation)
+ model_confidence: Уверенность модели (0-1)
+
+ Returns:
+ Dict с полным отчетом о качестве, включая:
+ - quality_class: 0 или 1
+ - violation_type: тип нарушения
+ - reason: человекочитаемое описание
+ - metrics: детальные метрики по каждому критерию
+ - overall_quality: GOOD/WARNING/POOR
+ - severity: NONE/LOW/MEDIUM/HIGH
"""
# Calculate all quality metrics
quality_metrics = {}
-
+
# Motion detection
quality_metrics["motion"] = detect_motion_blur(image, segmentation)
-
+
# Artifact detection
quality_metrics["artifacts"] = detect_artifacts(image, segmentation)
-
+
# Region-specific checks
if region == "spine":
quality_metrics["spine_completeness"] = check_spine_completeness(segmentation)
@@ -653,18 +779,18 @@ def generate_quality_report(
elif region in ("hip", "hip_left", "hip_right"):
quality_metrics["hip_completeness"] = check_hip_completeness(segmentation)
quality_metrics["hip_rotation"] = check_hip_rotation(segmentation, image)
-
+
# ROI check
quality_metrics["roi_check"] = check_roi_boundaries(segmentation, image.shape)
-
+
# Basic position check (from existing scorer)
from src.quality.quality_scorer import QualityScorer
scorer = QualityScorer()
quality_metrics["position"] = scorer.check_position(segmentation)
-
+
# Determine violation type
violation_type, reason = determine_violation_type(region, quality_metrics)
-
+
# Overall quality determination
if violation_type == "correct":
overall_quality = "GOOD"
@@ -674,7 +800,7 @@ def generate_quality_report(
overall_quality = "POOR"
severity = "HIGH"
quality_class = 1
-
+
# Determine view quality
if region == "spine":
completeness = quality_metrics.get("spine_completeness", {})
@@ -684,27 +810,27 @@ def generate_quality_report(
view_quality = "full" if completeness.get("valid", False) else "partial"
else:
view_quality = "unknown"
-
+
return {
# Basic info
"anatomical_region": region,
"quality_class": quality_class,
"quality_label": "OK" if quality_class == 0 else "Violation detected",
-
+
# New detailed fields
"violation_type": violation_type,
"reason": reason,
-
+
# Confidence
"confidence": model_confidence,
"confidence_per_class": {
"correct": float(model_confidence) if quality_class == 0 else float(1 - model_confidence),
"violation": float(1 - model_confidence) if quality_class == 0 else float(model_confidence)
},
-
+
# View quality
"view_quality": view_quality,
-
+
# Detailed metrics
"metrics": {
"motion": quality_metrics.get("motion", {}),
@@ -712,13 +838,13 @@ def generate_quality_report(
"position": quality_metrics.get("position", {}),
"roi_check": quality_metrics.get("roi_check", {})
},
-
+
# Region-specific
"spine_completeness": quality_metrics.get("spine_completeness", {}),
"vertebral_contours": quality_metrics.get("vertebral_contours", {}),
"hip_completeness": quality_metrics.get("hip_completeness", {}),
"hip_rotation": quality_metrics.get("hip_rotation", {}),
-
+
# Overall
"overall_quality": overall_quality,
"severity": severity
diff --git a/src/segmentators/totalsegmentator_wrapper.py b/src/segmentators/totalsegmentator_wrapper.py
index c123698..4ecae71 100644
--- a/src/segmentators/totalsegmentator_wrapper.py
+++ b/src/segmentators/totalsegmentator_wrapper.py
@@ -1,35 +1,147 @@
from src.segmentators.base import BaseSegmentator
import numpy as np
+import tempfile
+import os
+from pathlib import Path
+from typing import Literal
class TotalSegmentatorWrapper(BaseSegmentator):
- """Обертка для TotalSegmentator (для костей)"""
+ """Обертка для TotalSegmentator (костная сегментация)"""
- def __init__(self):
+ TASK_MAP = {
+ "spine": "vertebrae_body",
+ "hip": "appendicular_bones",
+ "hip_left": "appendicular_bones",
+ "hip_right": "appendicular_bones",
+ "vertebrae": "vertebrae_body",
+ "appendicular": "appendicular_bones",
+ "total": "total",
+ }
+
+ def __init__(
+ self,
+ device: str = "cpu",
+ task: str = "vertebrae_body",
+ anatomy_region: str = None
+ ):
+ """
+ Args:
+ device: устройство для инференса ('cpu', 'gpu', 'mps')
+ task: задача сегментации ('vertebrae_body', 'appendicular_bones', 'total')
+ anatomy_region: анатомическая область ('spine', 'hip', 'hip_left', 'hip_right')
+ """
+ # Определяем задачу на основе анатомической области
+ if anatomy_region and anatomy_region in self.TASK_MAP:
+ task = self.TASK_MAP[anatomy_region]
+
+ self.device = device
+ self.task = task
+ self.loaded = False
+ self.model = None
+ self.totalsegmentator_func = None
+
+ # Lazy load - откладываем до первого использования
+ # self._load_model()
+
+ def _load_model(self):
+ """Ленивая загрузка модели при первом вызове"""
+ if self.loaded:
+ return
+
try:
- from totalsegmentator import TotalSegmentator
- self.model = TotalSegmentator()
+ import nibabel as nib
+ from totalsegmentator.python_api import totalsegmentator
+
+ self.totalsegmentator_func = totalsegmentator
+ self.nibabel = nib
self.loaded = True
- print("✅ TotalSegmentator загружен")
- except ImportError:
- print("⚠️ TotalSegmentator не установлен")
+ print(f"✅ TotalSegmentator API загружен (task={self.task})")
+ except ImportError as e:
self.loaded = False
+ print(f"⚠️ TotalSegmentator: {e}")
+ except Exception as e:
+ self.loaded = False
+ print(f"⚠️ TotalSegmentator: ошибка инициализации - {e}")
def segment(self, image):
+ """
+ Сегментация 2D изображения (numpy array или PIL Image)
+
+ Args:
+ image: numpy array [H, W] или PIL Image
+
+ Returns:
+ Бинарная маска [H, W] где 1 = кость
+ """
+ # Ленивая загрузка модели при первом вызове
if not self.loaded:
- return np.zeros((512, 512), dtype=np.int64)
+ self._load_model()
+
+ if not self.loaded:
+ h, w = 512, 512
+ if hasattr(image, 'shape'):
+ if len(image.shape) >= 2:
+ h, w = image.shape[:2]
+ return np.zeros((h, w), dtype=np.int64)
- # TotalSegmentator ожидает DICOM или NIfTI
- # Здесь адаптация под входной формат
- # Реальная реализация зависит от TotalSegmentator API
+ # Конвертация в numpy если PIL
+ if hasattr(image, 'convert'):
+ image = np.array(image)
+
+ # Проверка что это 2D изображение
+ if len(image.shape) == 3:
+ image = image[:, :, 0] # Берем первый канал
+
+ # Сохраняем во временный файл как NIfTI
+ import nibabel as nib
+
+ # Создаем 3D объем (1 срез)
+ image_3d = np.expand_dims(image.astype(np.float32), axis=0)
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ input_path = Path(tmpdir) / "input.nii.gz"
+ output_path = Path(tmpdir) / "output"
+ output_path.mkdir()
+
+ # Сохраняем как NIfTI
+ nifti_img = nib.Nifti1Image(image_3d, np.eye(4))
+ nib.save(nifti_img, str(input_path))
+
+ # Запускаем TotalSegmentator
+ try:
+ result = self.totalsegmentator_func(
+ input=str(input_path),
+ output=str(output_path),
+ task=self.task,
+ device=self.device,
+ quiet=True,
+ verbose=False
+ )
+
+ # Загружаем результат
+ result_files = list(output_path.glob("*.nii.gz"))
+ if result_files:
+ result_nifti = nib.load(str(result_files[0]))
+ mask_3d = result_nifti.get_fdata()
+ # Берем первый (и единственный) срез
+ if mask_3d.ndim == 3:
+ mask_2d = mask_3d[0, :, :]
+ else:
+ mask_2d = mask_3d
+ return (mask_2d > 0).astype(np.int64)
+
+ except Exception as e:
+ pass # Молча возвращаем пустую маску
- # Пока возвращаем заглушку
- return np.ones((512, 512), dtype=np.int64)
+ return np.zeros(image.shape, dtype=np.int64)
def get_info(self):
return {
"name": "TotalSegmentator",
"type": "segmentation",
"target": "bones",
+ "task": self.task,
+ "device": self.device,
"loaded": self.loaded
}
\ No newline at end of file