bone_2026/README.md

294 lines
19 KiB
Markdown
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.

# 🦴 DXA Quality Assessment
Сервис автоматизированного контроля качества денситометрических исследований (DXA):
принимает DICOM, определяет анатомическую область, оценивает, пригодно ли изображение
для клинической интерпретации, и формирует структурированный отчёт.
## Что делает решение
| Шаг | Реализация |
|---|---|
| Определение анатомической области | Ширина кадра (позвоночник / бедро) + голова области + геометрия яркой зоны |
| Бинарная оценка качества | ResNet18 (ImageNet) → линейная голова; порог подобран по F1 на валидации |
| Тип нарушения | Общая категория для снимков с нарушением; детальный тип требует разметки типов на уровне снимка |
| Отчёт | 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 в файлах нет,
поэтому корректность нанесённых областей измерения нельзя проверить прямым
сравнением — оценивается только геометрия видимой зоны.
6. **Эвристики из `src/quality/detailed_assessment.py` не калиброваны.**
Пороги для «движения», «артефактов» и отступов ROI рассчитаны на другой
масштаб интенсивностей: на этом наборе они срабатывают почти для любого
снимка, а признак резкости ведёт себя противоположно в позвоночнике и
бёдрах. Поэтому в панели деталей показываются **числовые измерения** с
пометкой «справ.», а не вердикты «Да/Нет». Классификацию выполняет только
модель. Проверить её вклад можно командой:
```bash
python -m src.dxa.discriminator --model-path models/dxa_model.pth
```
## Что проверено и как
| Проверка | Команда | Результат |
|---|---|---|
| Модель использует снимок, а не только область | `python -m src.dxa.discriminator` | AUC 0.82 против 0.72 у правила «позвоночник = нарушение»; внутри областей у модели 0.84–0.95, у правила 0.50 |
| Контракт API для веб-интерфейса | `python -m pytest tests/test_api_contract.py` | 15 тестов: поля панели деталей, различимость метрик, PNG-визуализации, отсутствие некалиброванных вердиктов |
| Разметка и разбиение данных | `python -m pytest tests/test_labels.py` | метки из имён, склейка дублей, отсутствие утечки между train/val |
| Метрики и порог | `python -m pytest tests/test_preprocess_and_model.py` | подбор порога при дисбалансе, roundtrip чекпоинта, BatchNorm |
| Веб-интерфейс в браузере | `node tests/browser/ui_check.js` | значения панели меняются при переключении строк |
| Работа без сети | `node tests/browser/ui_offline.js` | ноль внешних запросов, стили и иконки на месте |
Браузерные проверки требуют запущенного сервера:
```bash
python -m uvicorn src.main:app --port 8123
node tests/browser/ui_check.js # панель деталей обновляется по клику
node tests/browser/ui_offline.js # работа без доступа к внешним сервисам
```
Офлайн-режим обеспечен локальными копиями Tailwind и FontAwesome
(`src/api/static/vendor`, `src/api/static/webfonts`); страница не обращается к CDN.
## План доработки
- Разметить типы нарушений на уровне снимка и обучить мультилейбл-классификатор.
- Собрать 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>