261 lines
16 KiB
Markdown
261 lines
16 KiB
Markdown
# 🦴 DXA Quality Assessment
|
||
|
||
Сервис автоматизированного контроля качества денситометрических исследований (DXA):
|
||
принимает DICOM, определяет анатомическую область, оценивает, пригодно ли изображение
|
||
для клинической интерпретации, и формирует структурированный отчёт.
|
||
|
||
## Что делает решение
|
||
|
||
| Шаг | Реализация |
|
||
|---|---|
|
||
| Определение анатомической области | Ширина кадра (позвоночник / бедро) + голова области + геометрия яркой зоны |
|
||
| Бинарная оценка качества | ResNet18 (ImageNet) → линейная голова; порог подобран по F1 на валидации |
|
||
| Тип нарушения | Эвристики по изображению: размытие/движение, посторонние включения, геометрия ROI |
|
||
| Отчёт | XLSX/CSV со столбцами из требований; опционально zip с визуализацией зоны интереса |
|
||
| API | FastAPI: анализ, детальный анализ, пакетная обработка, экспорт, DICOM SR (текст) |
|
||
| Веб-интерфейс | Загрузка DICOM, таблица результатов |
|
||
|
||
## Установка и запуск
|
||
|
||
```bash
|
||
python3 -m venv venv && source venv/bin/activate
|
||
pip install -r requirements.txt
|
||
|
||
./run.sh train # обучить модель качества
|
||
./run.sh infer "dataset_hack/Для теста" results.xlsx # пакетная обработка
|
||
./run.sh serve # API и веб-интерфейс на :8000
|
||
./run.sh test # тесты
|
||
```
|
||
|
||
Для инференса только на CPU (образ меньше, без CUDA-колёс):
|
||
|
||
```bash
|
||
pip install -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cpu
|
||
```
|
||
|
||
Обучение и инференс можно вызывать напрямую:
|
||
|
||
```bash
|
||
python -m src.dxa.train --epochs 100 --output-dir models
|
||
python -m src.dxa.inference --input-path dataset_hack --output-path results.xlsx --zip-out masks.zip
|
||
```
|
||
|
||
После запуска сервера:
|
||
- Веб-интерфейс: http://localhost:8000
|
||
- Swagger UI: http://localhost:8000/docs
|
||
|
||
## Docker
|
||
|
||
```bash
|
||
docker build -t dxa-quality .
|
||
docker run -v /path/to/data:/data -p 8000:8000 dxa-quality
|
||
```
|
||
|
||
Чекпоинт должен лежать в `models/dxa_model.pth` до сборки; путь задаётся переменной
|
||
`DXA_MODEL_PATH` (по умолчанию `/app/models/dxa_model.pth`). Сборка проверяет, что
|
||
чекпоинт читается, и падает, если модели нет — вместо тихих 500-х ответов в рантайме.
|
||
Вес модели внутрь образа зашит, из сети ничего не скачивается.
|
||
|
||
---
|
||
|
||
## Архитектура
|
||
|
||
```
|
||
DICOM ──▶ предобработка ──▶ ResNet18 (заморожен) ──▶ линейная голова ──▶ логит
|
||
│ │
|
||
│ └──▶ голова области (вспомогательная)
|
||
│
|
||
├──▶ геометрия яркой зоны: область, ROI, геометрия кадра
|
||
└──▶ эвристики: резкость, «плотные» включения
|
||
```
|
||
|
||
Ключевые решения и почему они такие:
|
||
|
||
1. **Линейный зонд вместо полного fine-tune.** Уникальных снимков в наборе ~250.
|
||
Полный fine-tune ResNet18 переобучается за несколько эпох (train F1 → 1.0 при
|
||
val AUC ≈ 0.5). Замороженный backbone + линейная голова удерживает val AUC
|
||
≈ 0.7–0.85. Режим `--head mlp --freeze-epochs 0` оставлен для экспериментов
|
||
на большем объёме данных.
|
||
|
||
2. **Метки из имён файлов.** Суффикс `_good`/`_bad` — экспертная оценка снимка;
|
||
отсутствие суффикса означает «изображение хорошее». Приоритет:
|
||
`_bad` > `_good` > нет метки.
|
||
|
||
3. **Склейка побайтных дублей.** В датасете 544 файла, но 252 уникальных снимка:
|
||
один и тот же кадр сохранён многократно под разными именами (часть — с меткой,
|
||
часть — без). Без склейки одно изображение попадало бы в оба класса.
|
||
|
||
4. **Разбиение по исследованиям.** Снимки одного исследования не попадают
|
||
одновременно в train и val — иначе метрики завышаются за счёт утечки.
|
||
|
||
5. **Аугментация отключена.** Проверено экспериментально: яркостный разброс и сдвиг
|
||
кадра снижают AUC с 0.87 до 0.56, потому что распределение яркости и положение
|
||
области сами являются признаками качества. Флаг `--augment` включает её для
|
||
экспериментов.
|
||
|
||
6. **Порог по логиту.** При доле нарушений ~15 % порог 0.5 даёт нулевой recall.
|
||
Порог подбирается по F1 на валидации и сохраняется в чекпоинт; решение
|
||
принимается по логиту (численно устойчиво при насыщении вероятностей).
|
||
|
||
---
|
||
|
||
## Формат выходных данных
|
||
|
||
Основные столбцы соответствуют требованиям задания:
|
||
|
||
| Столбец | Описание |
|
||
|---|---|
|
||
| `path_to_study` | Путь к исследованию (для HTTP-загрузки — `upload://<имя>`) |
|
||
| `study_uid` | StudyInstanceUID |
|
||
| `image_uid` | SOPInstanceUID |
|
||
| `anatomical_region` | `spine` / `hip_left` / `hip_right` / `hip` |
|
||
| `quality_class` | 0 — качественное, 1 — есть нарушение |
|
||
| `violation_type` | Тип нарушения или пустая строка |
|
||
| `processing_status` | `Success` или `Failure: <причина>` |
|
||
| `time_of_processing` | Время обработки, секунды |
|
||
|
||
Дополнительно добавляются `confidence`, `violation_reason`, `region_confidence`
|
||
— они не мешают автоматическому разбору обязательных столбцов.
|
||
|
||
## API
|
||
|
||
| Метод | Путь | Назначение |
|
||
|---|---|---|
|
||
| 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 -F "file=@study/spine.dcm"
|
||
```
|
||
|
||
```json
|
||
{
|
||
"study_uid": "1.2.643...",
|
||
"image_uid": "1.2.643...",
|
||
"anatomical_region": "spine",
|
||
"quality_class": 1,
|
||
"quality_label": "Violation detected",
|
||
"violation_type": "quality_violation_detected",
|
||
"reason": "Выявлено нарушение качества изображения",
|
||
"confidence": 0.72,
|
||
"threshold_probability": 0.6154,
|
||
"processing_status": "Success"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Метрики
|
||
|
||
Метрики зависят от выбранного разбиения по исследованиям, поэтому приводятся
|
||
с разбросом. Оценка на валидационной части (19 исследований, 51 снимок,
|
||
8 нарушений), разбиение по исследованиям:
|
||
|
||
| Что измерено | Значение | Как измерено |
|
||
|---|---|---|
|
||
| ROC-AUC, 5 разбиений | **0.76 ± 0.08** (0.64 – 0.84) | обучение по seed 0..4, порог по F1 |
|
||
| PR-AUC, 5 разбиений | 0.48 ± 0.17 | там же; базовый уровень при 15 % нарушений — 0.15 |
|
||
| F1, 5 разбиений | 0.54 ± 0.11 | там же (порог подобран на той же валидации — смещено вверх) |
|
||
| ROC-AUC, 5-фолдовая CV | **0.81 ± 0.08** | линейный зонд на тех же признаках, разбиение по исследованиям |
|
||
| Контрольная задача «позвоночник / бедро» | AUC 1.00 | проверка работоспособности пайплайна |
|
||
| Перестановка меток (нулевая гипотеза) | AUC 0.64 | вклад случайных корреляций |
|
||
|
||
Метрики по областям — в `models/train_report.md`, он создаётся при обучении.
|
||
Разбивка важна, потому что нарушения распределены крайне неравномерно: в
|
||
позвоночнике ~29 % снимков с нарушением против ~4–5 % у бёдер, а область почти
|
||
однозначно определяется по ширине кадра. Поэтому общий AUC частично отражает
|
||
различение области, а не только распознавание дефекта.
|
||
|
||
Время обработки одного снимка — порядка 0.02–0.05 с на CPU (ResNet18 с
|
||
замороженным backbone), то есть требование «не более 3 минут на исследование»
|
||
выполняется с большим запасом.
|
||
|
||
---
|
||
|
||
## Ограничения (важно для интерпретации)
|
||
|
||
1. **Разметка исходных данных — на уровне исследования, а не снимка.**
|
||
В наборе один снимок помечен `_bad`, остальные снимки того же исследования
|
||
не размечены. Метка снимка считается унаследованной от исследования, поэтому
|
||
часть меток заведомо шумная.
|
||
2. **Мало данных.** 252 уникальных снимка, 37 нарушений. Доверительные интервалы
|
||
широкие; оценка на закрытом наборе может отличаться.
|
||
3. **Тип нарушения определяется эвристиками, а не обученной моделью.** Для
|
||
честного мультикласса нужна разметка типов на уровне снимка.
|
||
4. **Область определяется по размеру кадра.** Признак безошибочно работает на этом
|
||
оборудовании (99/99 для позвоночника), но при смене аппарата порог
|
||
`SPINE_MIN_WIDTH` потребует калибровки.
|
||
5. **В DICOM нет разметки ROI.** Ни overlay, ни graphic annotation в файлах нет,
|
||
поэтому корректность нанесённых областей измерения нельзя проверить прямым
|
||
сравнением — оценивается только геометрия видимой зоны.
|
||
|
||
## План доработки
|
||
|
||
- Разметить типы нарушений на уровне снимка и обучить мультилейбл-классификатор.
|
||
- Собрать 500+ исследований для устойчивых метрик и честной валидации.
|
||
- Подключить Grad-CAM для объяснения решения (модуль есть, но не интегрирован).
|
||
- Заменить порог по ширине кадра на калибровку по метаданным аппарата.
|
||
|
||
---
|
||
|
||
## Структура проекта
|
||
|
||
```
|
||
bone_2026/
|
||
├── src/
|
||
│ ├── main.py # FastAPI: маршруты и загрузка модели
|
||
│ ├── dxa/ # действующий модуль оценки качества
|
||
│ │ ├── labels.py # разбор имён, метки, склейка дублей, сплит
|
||
│ │ ├── preprocess.py # DICOM -> тензор (общий для обучения и API)
|
||
│ │ ├── dataset.py # Dataset и DataLoader
|
||
│ │ ├── model.py # сеть, метрики, подбор порога
|
||
│ │ ├── train.py # обучение и отчёт
|
||
│ │ └── inference.py # пакетный инференс, определение области
|
||
│ ├── quality/ # эвристики (частично используются API)
|
||
│ ├── api/static/ # веб-интерфейс
|
||
│ └── model/, core/, pipeline/ # устаревшие модули, не подключены к API
|
||
├── models/dxa_model.pth # чекпоинт (+ train_report.md)
|
||
├── tests/ # pytest: метки, сплит, метрики, модель
|
||
├── dataset_hack/ # данные (в git не хранятся)
|
||
├── Dockerfile
|
||
├── requirements.txt
|
||
└── run.sh
|
||
```
|
||
|
||
## Запуск обучения
|
||
|
||
```bash
|
||
python -m src.dxa.train --epochs 100 --output-dir models
|
||
```
|
||
|
||
| Параметр | По умолчанию | Описание |
|
||
|---|---|---|
|
||
| `--data-root` | `dataset_hack` | Каталог датасета |
|
||
| `--annotation-path` | `dataset_hack/НД_для_обучения/разметка.xlsx` | Excel с разметкой (только отчёт о расхождениях) |
|
||
| `--backbone` | `resnet18` | `resnet18` / `resnet34` |
|
||
| `--head` | `linear` | `linear` (линейный зонд) / `mlp` |
|
||
| `--freeze-epochs` | `-1` | `-1` — backbone заморожен всегда; `0` — обучать всю сеть |
|
||
| `--epochs`, `--batch-size`, `--learning-rate`, `--weight-decay` | 100 / 16 / 3e-4 / 5e-2 | Оптимизация |
|
||
| `--balance` | `none` | `loss` / `sampler` для компенсации дисбаланса |
|
||
| `--val-fraction`, `--seed` | 0.2 / 42 | Разбиение по исследованиям |
|
||
| `--augment` | выключено | Включает яркостную аугментацию (ухудшает метрики, см. п. 5) |
|
||
| `--output-dir` | `models` | Куда сохранять чекпоинт и отчёты |
|
||
| `--dry-run` | — | Проверить разбор данных и разбиение без обучения |
|
||
|
||
После обучения в `--output-dir` появляются `dxa_model.pth`, `train_report.md`
|
||
и `train_report.json`; отчёт удобно приложить к презентации.
|
||
|
||
## Команда
|
||
|
||
- **Грачев Денис** — разработка
|
||
- **Грачев Татьяна** — капитан
|
||
|
||
<div align="center">
|
||
<sub>Built for Bone Quality Assessment Hackathon 2026</sub>
|
||
</div>
|