529 lines
42 KiB
Markdown
529 lines
42 KiB
Markdown
# 🦴 DXA Quality Assessment
|
||
|
||
Сервис автоматизированного контроля качества денситометрических исследований (DXA):
|
||
принимает DICOM, определяет анатомическую область, оценивает, пригодно ли изображение
|
||
для клинической интерпретации, и формирует структурированный отчёт.
|
||
|
||
<div align="center">
|
||
<img src="assets/img/ui-results.png" alt="Результаты обработки в веб-интерфейсе: полоса состояния, статистика и таблица снимков" width="90%">
|
||
</div>
|
||
|
||
## Что делает решение
|
||
|
||
| Шаг | Реализация |
|
||
|---|---|
|
||
| Определение анатомической области | Ширина кадра (позвоночник / бедро) + голова области + геометрия яркой зоны |
|
||
| Бинарная оценка качества | 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 label # разметить датасет по Excel
|
||
./run.sh rename # план приведения имён файлов
|
||
./run.sh train # обучить модель качества
|
||
./run.sh infer "dataset_hack/Для теста" results.xlsx # пакетная обработка
|
||
./run.sh serve # API и веб-интерфейс на :8000
|
||
./run.sh split && ./run.sh compare # сравнить варианты разметки
|
||
./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 -v /path/to/models:/app/models -p 8000:8000 dxa-quality
|
||
```
|
||
|
||
Чекпоинт в образ не копируется: он монтируется в `/app/models`, путь задаётся
|
||
переменной `DXA_MODEL_PATH` (по умолчанию `/app/models/dxa_model.pth`). Без
|
||
чекпоинта сервис всё равно поднимается — `/api/v1/health` сообщает
|
||
`model_loaded: false`, а анализ отвечает ошибкой `Model not loaded` вместо тихой
|
||
неверной оценки. Из сети ничего не скачивается. Сборка проверяет, что приложение
|
||
импортируется и что офлайн-ассеты фронтенда на месте.
|
||
|
||
---
|
||
|
||
## Архитектура
|
||
|
||
<div align="center">
|
||
<img src="assets/img/architecture-pipeline.png" alt="Пайплайн обработки DXA: загрузка DICOM, разбор метаданных, предобработка, инференс, определение области, метрики качества, отчёт" width="52%">
|
||
</div>
|
||
|
||
Схема работающего пути в терминах решения:
|
||
|
||
```
|
||
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. **Метки на уровне снимка.** Источник — `labels/labels_images.csv`, построенный
|
||
из экспертной таблицы командой `./run.sh label` (разбор — в
|
||
`assets/labeling.md`): в таблице отмечены критерии качества по каждому
|
||
исследованию, а каждая область встречается в нём ровно один раз, поэтому
|
||
вердикт переносится на снимок однозначно. Такой разметки — 76 нарушений из
|
||
251 (30.3 %). Правило выбрано измерением: учёт ручных пометок из имён файлов
|
||
дал худший результат на held-out наборе, поэтому в метках они не участвуют.
|
||
Резервный режим `--labels-csv ""` берёт метку из суффикса `_good`/`_bad` и
|
||
оставлен для совместимости.
|
||
|
||
3. **Склейка побайтных дублей.** В датасете 482 файла, но 251 уникальный снимок:
|
||
один и тот же кадр сохранён многократно под разными именами (часть — с меткой,
|
||
часть — без). Без склейки одно изображение попадало бы в оба класса. Лишние
|
||
байт-идентичные копии удалены.
|
||
|
||
4. **Разбиение по исследованиям.** Снимки одного исследования не попадают
|
||
одновременно в train и val — иначе метрики завышаются за счёт утечки.
|
||
|
||
5. **Аугментация отключена.** Проверено экспериментально: яркостный разброс и сдвиг
|
||
кадра снижают AUC с 0.87 до 0.56, потому что распределение яркости и положение
|
||
области сами являются признаками качества. Флаг `--augment` включает её для
|
||
экспериментов.
|
||
|
||
6. **Порог по логиту.** При доле нарушений ~15 % порог 0.5 даёт нулевой recall.
|
||
Порог подбирается по F1 на валидации и сохраняется в чекпоинт; решение
|
||
принимается по логиту (численно устойчиво при насыщении вероятностей).
|
||
|
||
### Схемы системы
|
||
|
||
| Компоненты | Поток данных |
|
||
|---|---|
|
||
| <img src="assets/img/architecture-components.png" width="100%" alt="Диаграмма компонентов: веб-клиент, FastAPI, модели DXA, оценка качества, файловая система"> | <img src="assets/img/architecture-dataflow.png" width="100%" alt="Поток данных: вход, предобработка, модели, анализ качества, выход"> |
|
||
|
||
| Последовательность детального анализа | Развёртывание |
|
||
|---|---|
|
||
| <img src="assets/img/architecture-sequence.png" width="100%" alt="Диаграмма последовательности: запрос /analyze/detailed, предобработка, инференс, регион, сегментация, отчёт"> | <img src="assets/img/architecture-deployment.png" width="100%" alt="Диаграмма развёртывания: клиент, приложение, ML-пайплайн, инфраструктура"> |
|
||
|
||
| Определение анатомической области | Основные сущности |
|
||
|---|---|
|
||
| <img src="assets/img/architecture-region-detection.png" width="100%" alt="Алгоритм определения области: порог по перцентилю яркости, bounding box, соотношение сторон, сторона бедра"> | <img src="assets/img/architecture-classes.png" width="100%" alt="Диаграмма классов: классификатор, модель, детальная оценка, определение области"> |
|
||
|
||
Схемы взяты из проектного документа и показывают целевую архитектуру: блоки
|
||
`Orchestrator`, UNet-сегментации и готовых вердиктов качества в решении не
|
||
реализованы. Работающий путь описан выше.
|
||
|
||
---
|
||
|
||
## Формат выходных данных
|
||
|
||
Основные столбцы соответствуют требованиям задания:
|
||
|
||
| Столбец | Описание |
|
||
|---|---|
|
||
| `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 | `/label` | Интерфейс ручной разметки: подтверждение и правка вердиктов специалистом |
|
||
| GET | `/api/v1/health` | Статус, признак загрузки модели и её происхождение |
|
||
| GET | `/api/v1/model` | Карточка решения: разметка, данные, метрики с интервалами, словарь нарушений |
|
||
| 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 | `/api/v1/labeling/items` | Снимки датасета для разбора: текущая метка, её источник, расхождения |
|
||
| GET | `/api/v1/labeling/image` | PNG снимка для просмотра |
|
||
| POST | `/api/v1/labeling/verdict` | Сохранить вердикт специалиста |
|
||
| DELETE | `/api/v1/labeling/verdict` | Снять вердикт, вернуть снимок к построенной разметке |
|
||
| GET | `/api/v1/labeling/export` | Выгрузка разметки: `scope=all` принимается обучением как `--labels-csv` |
|
||
|
||
```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_type": "artifact",
|
||
"violation_type_label": "Артефакты и импланты",
|
||
"violation_type_is_heuristic": true,
|
||
"violation_type_note": "Тип нарушения определён эвристикой по метрикам снимка, а не моделью...",
|
||
"reason": "Посторонние включения или артефакты в зоне интереса",
|
||
"confidence": 0.72,
|
||
"threshold_probability": 0.5103,
|
||
"processing_status": "Success"
|
||
}
|
||
```
|
||
|
||
Коды `violation_type` берутся из единого словаря `src/dxa/violations.py`
|
||
(`positioning`, `axis_deviation`, `artifact`, `rotation`, `roi_incorrect`,
|
||
`motion`, `incomplete_anatomy`, `labeling_error`, `unspecified`) — первые пять
|
||
кодирует экспертная таблица, остальные приходят из критериев пригодности. Поле
|
||
`violation_type_is_heuristic` показывает, что тип определён эвристикой, а не
|
||
моделью: модель решает только бинарную задачу. Подписи для интерфейса отдаёт
|
||
сервер, своей копии словаря фронтенд не держит.
|
||
|
||
---
|
||
|
||
## Веб-интерфейс
|
||
|
||
Полоса состояния в шапке показывает, **какая** модель сейчас работает: имя
|
||
чекпоинта, источник разметки, эпоху и порог. По интерфейсу сразу видно, какая
|
||
версия решения отвечает, — без обращения к логам.
|
||
|
||
<div align="center">
|
||
<img src="assets/img/ui-banner.png" alt="Полоса состояния: загруженная модель, разметка, эпоха, порог" width="90%">
|
||
</div>
|
||
|
||
Строки таблицы результатов кликабельны, но об этом надо сказать прямо — иначе
|
||
не догадываются. Над таблицей висит подсказка «Нажмите на любую строку», в конце
|
||
каждой строки есть кнопка «Открыть», а клик по строке прокручивает страницу к
|
||
панели деталей. Подсказка скрывается, когда фильтр не оставил ни одной строки.
|
||
Кнопка «Открыть» — настоящая кнопка, поэтому панель доступна и с клавиатуры
|
||
(Tab + Enter), а не только мышью. Сама таблица со статистикой и подсказкой — на
|
||
первом скриншоте.
|
||
|
||
Панель деталей открывается по клику на строку: заключение, измерения и
|
||
визуализация снимка. Специально показаны обе ветки — «нарушение» и «норма»:
|
||
|
||
<table>
|
||
<tr>
|
||
<td width="50%"><img src="assets/img/ui-detail-violation.png" width="100%" alt="Панель деталей для снимка с нарушением: красный бейдж, уровень HIGH, заключение с вероятностью и порогом, визуализация"></td>
|
||
<td width="50%"><img src="assets/img/ui-detail-clean.png" width="100%" alt="Панель деталей для качественного снимка: зелёный бейдж, справочные измерения"></td>
|
||
</tr>
|
||
<tr>
|
||
<td><sub><b>Нарушение.</b> Бейдж «Нарушение», уровень HIGH, заключение с вероятностью и порогом, тип нарушения с пометкой «эвристика».</sub></td>
|
||
<td><sub><b>Норма.</b> Зелёный бейдж. Измерения (резкость, границы области) подписаны как справочные — их пороги не калиброваны.</sub></td>
|
||
</tr>
|
||
</table>
|
||
|
||
Панель **«О модели»** (кнопка в шапке): что за чекпоинт работает, на какой
|
||
разметке он обучен, с каким порогом решает, метрики сравнения с 95 % интервалами,
|
||
состав данных, словарь типов нарушений и список ограничений.
|
||
|
||
<div align="center">
|
||
<img src="assets/img/ui-model-panel.png" alt="Панель «О модели»: сведения о чекпоинте, метрики с интервалами, состав данных, словарь нарушений" width="90%">
|
||
</div>
|
||
|
||
Источник всех этих значений — сервер (`/api/v1/health`, `/api/v1/model`).
|
||
Фронтенд намеренно не держит собственных копий: раньше подписи типов нарушений
|
||
были продублированы в `dxa-app.js` и разошлись с серверными, из-за чего коды
|
||
экспертной таблицы показывались как есть.
|
||
|
||
Что интерфейс теперь не утверждает:
|
||
|
||
- **тип нарушения помечен как «эвристика»** с пояснением — модель решает только
|
||
бинарную задачу и тип не предсказывает;
|
||
- **метрика рабочего чекпоинта помечена как завышенная** прямо в баннере
|
||
состояния и в карточке: чекпоинт выбран лучшим из пяти seed'ов, честная
|
||
оценка варианта — среднее по seed'ам;
|
||
- **плитка «Ср. уверенность модели»** больше не называется точностью: точность
|
||
требует эталонных меток, которых для произвольного файла нет;
|
||
- числовые метрики в панели деталей по-прежнему идут под дисклеймером о
|
||
некалиброванности порогов.
|
||
|
||
### Ручная разметка (`/label`)
|
||
|
||
Поштучной экспертной оценки снимков в наборе нет: таблица оценивает исследование,
|
||
и снимок наследует вердикт исследования. Закрыть это автоматически нечем —
|
||
локальная vision-модель оказалась непригодна (`assets/labeling.md` §11). Поэтому
|
||
в сервисе есть отдельный интерфейс ручной разметки: раздел 2.6 задания просит
|
||
«автоматическую коррекцию разметки с возможностью подтверждения специалистом».
|
||
|
||
Откройте `http://localhost:8000/label`. Интерфейс показывает снимок целиком и его
|
||
текущую метку с указанием источника (`экспертная таблица`, `имя файла`, `ручной
|
||
вердикт`). Снимки, где метка разметки расходится с пометкой в имени файла, идут
|
||
первыми: там ошибка возможна в любом из источников. Специалист подтверждает
|
||
вердикт или ставит свой — область, «годное / нарушение», тип нарушения,
|
||
комментарий.
|
||
|
||
Вердикты сохраняются в `labels/manual_labels.csv` в том же формате, что и
|
||
построенная разметка, поэтому файл можно сразу передать обучению:
|
||
|
||
```bash
|
||
curl -o manual_labels.csv "http://localhost:8000/api/v1/labeling/export?scope=all"
|
||
python -m src.dxa.train --labels-csv manual_labels.csv
|
||
```
|
||
|
||
`scope=all` накладывает ручные вердикты на построенную разметку (весь набор),
|
||
`scope=reviewed` отдаёт только разобранные снимки — для сверки с построенной
|
||
разметкой. Оценка модели в интерфейсе намеренно не показывается: подсказка
|
||
смещала бы разметчика, а ценность здесь именно в независимом суждении.
|
||
|
||
### Передать разметку врачу: автономный HTML-пакет
|
||
|
||
Интерфейс `/label` требует запущенного сервиса, а размечать должен специалист — на
|
||
своей машине, без Python и без сети. Поэтому тот же сценарий упаковывается в один
|
||
файл: снимки встроены внутрь, вердикты хранятся в браузере, выгрузка — CSV.
|
||
|
||
```bash
|
||
./run.sh review --out review/doctor_review.html # весь набор, ~7 МБ
|
||
./run.sh review --limit 20 --out review/pilot.html # пилот на выборке из 20 снимков
|
||
```
|
||
|
||
Файл врач открывает двойным щелчком: снимок, построенная метка с указанием
|
||
источника, кнопки «годное / нарушение», тип нарушения, комментарий, горячие
|
||
клавиши. Работает полностью офлайн — страница не делает ни одного сетевого
|
||
запроса, и это проверяется автоматически (`tests/browser/review_pack.js`).
|
||
Прогресс сохраняется в браузере, а кнопка «Выгрузить CSV» отдаёт файл, который
|
||
принимается обучением как есть; загрузить его обратно можно на другой машине —
|
||
кнопкой «Загрузить CSV».
|
||
|
||
Вердикты врача накладываются на построенную разметку одной командой, с проверкой,
|
||
что все пути есть в датасете:
|
||
|
||
```bash
|
||
./run.sh review --merge review/doctor.csv --out labels/labels_images_reviewed.csv
|
||
python -m src.dxa.train --labels-csv labels/labels_images_reviewed.csv
|
||
```
|
||
|
||
Каталог `review/` в git не хранится: пакет генерируется, а внутри — медицинские
|
||
снимки.
|
||
|
||
---
|
||
|
||
## Метрики
|
||
|
||
Метрики зависят от выбранного разбиения по исследованиям, поэтому приводятся
|
||
с разбросом. Основная оценка — на **фиксированном** разбиении по исследованиям
|
||
(обучение 198 снимков / 81 исследование, валидация 53 снимка / 19 исследований,
|
||
16 нарушений), пять seed'ов обучения, эталон — вердикт эксперта:
|
||
|
||
| Что измерено | Значение | Как измерено |
|
||
|---|---|---|
|
||
| ROC-AUC | **0.6726** [0.6367, 0.7086] | 5 seed'ов на фиксированном разбиении |
|
||
| PR-AUC | 0.4753 [0.4188, 0.5318] | там же; базовый уровень при 30 % нарушений — 0.30 |
|
||
| F1 | 0.5559 [0.5212, 0.5906] | там же; порог подобран на той же валидации — смещено вверх |
|
||
| ROC-AUC по областям | позвоночник 0.929, бедро R 0.606, бедро L 0.567 | рабочий чекпоинт, собственная валидация |
|
||
| Контрольная задача «позвоночник / бедро» | AUC 1.00 | проверка работоспособности пайплайна |
|
||
| Модель использует снимок, а не область | AUC 0.885 против 0.534 у правила области | `discriminator` на всём наборе, включая обучающие снимки |
|
||
|
||
Метрики по областям — в `models/train_report.md`, он создаётся при обучении.
|
||
Разбивка важна, потому что нарушения распределены неравномерно: в позвоночнике
|
||
33 из 99, у бёдер 20–22 из 73–78, а область почти однозначно определяется по
|
||
ширине кадра. Поэтому общий AUC частично отражает различение области, а не только
|
||
распознавание дефекта.
|
||
|
||
### Выбор правила разметки
|
||
|
||
Метку можно было строить только из экспертной таблицы или дополнительно
|
||
учитывать пометки, проставленные вручную в именах файлов (суффикс `_bad`).
|
||
Пометки расходились с оценкой эксперта в 62 случаях из 251, поэтому правило
|
||
выбиралось измерением: одно разбиение, пять seed'ов, один эталон.
|
||
|
||
| Метрика (эталон) | только таблица | с суффиксами имён |
|
||
|---|---|---|
|
||
| ROC-AUC | **0.6726** [0.6367, 0.7086] | 0.6003 [0.5592, 0.6415] |
|
||
| PR-AUC | **0.4753** [0.4188, 0.5318] | 0.3864 [0.3487, 0.4242] |
|
||
| F1 | **0.5559** [0.5212, 0.5906] | 0.5354 [0.4978, 0.5729] |
|
||
| Recall / Precision | 0.713 / 0.472 | 0.788 / 0.409 |
|
||
|
||
Парная разница (только таблица − с суффиксами): ROC-AUC **+0.0723**
|
||
[+0.0541, +0.0905], PR-AUC **+0.0889** [+0.0500, +0.1277] — знаки `+++++`, то
|
||
есть преимущество на всех пяти seed'ах.
|
||
|
||
**Принято правило «только экспертная таблица».** Пометки в именах файлов
|
||
проставлялись вручную и оказались ненадёжными: они ухудшали согласие модели с
|
||
экспертом на невиданных исследованиях. Оговорка: эталон — та же таблица, поэтому
|
||
вариант, обучавшийся на ней, в выигрышном положении; значимо то, что добавление
|
||
ненадёжных пометок согласие **снижает**.
|
||
|
||
Отчёт с полными числами и разбором ограничений — `assets/labeling.md`; отчёт
|
||
обучения рабочего чекпоинта — `models/train_report.json`. Само сравнение правил
|
||
хранится только в виде чисел в этих документах: чекпоинты прогонов удалены,
|
||
а пересобрать их можно командой `./run.sh split && ./run.sh compare`.
|
||
|
||
Время обработки одного снимка — порядка 0.02–0.05 с на CPU (ResNet18 с
|
||
замороженным backbone), то есть требование «не более 3 минут на исследование»
|
||
выполняется с большим запасом.
|
||
|
||
---
|
||
|
||
## Ограничения (важно для интерпретации)
|
||
|
||
1. **Разметка выведена из оценки исследования.** Экспертная таблица описывает
|
||
исследование, а не снимок; перенос однозначен (область встречается один раз),
|
||
но поштучной экспертной оценки снимков в наборе нет. Происхождение каждой
|
||
строки зафиксировано в `labels/labels_images.csv`. Для поштучной разметки есть
|
||
интерфейс `/label` (см. «Веб-интерфейс»), им ещё не пользовались. Разбор — в
|
||
`assets/labeling.md`.
|
||
2. **Мало данных.** 251 уникальный снимок, 76 нарушений. Доверительные интервалы
|
||
широкие; оценка на закрытом наборе может отличаться.
|
||
3. **Тип нарушения определяется эвристиками, а не обученной моделью.** Для
|
||
честного мультикласса нужна разметка типов на уровне снимка — её даёт `/label`.
|
||
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.885 против 0.534 у правила «позвоночник = нарушение»; внутри областей у модели 0.88–0.95, у правила 0.50 |
|
||
| Контракт API для веб-интерфейса | `python -m pytest tests/test_api_contract.py` | поля панели деталей, различимость метрик, PNG-визуализации, отсутствие некалиброванных вердиктов, канонические коды типов нарушений, карточка модели |
|
||
| Ручная разметка | `python -m pytest tests/test_manual_labels.py tests/test_labeling_api.py` | проверка вердикта (область, метка, совместимость типа нарушения с областью), хранение и наложение на построенную разметку, отказ отдавать файлы вне датасета, выгрузка, читаемая обучением |
|
||
| Пакет разметки для врача | `python -m pytest tests/test_review_pack.py` | отпечаток набора, порядок «расхождения первыми», встроенные данные, отсутствие сетевых ссылок в странице, слияние вердиктов с разметкой |
|
||
| Единый словарь нарушений | `python -m pytest tests/test_violations.py` | коды, подписи, коды SR, приведение устаревших значений, согласованность с таблицей |
|
||
| Разметка и разбиение данных | `python -m pytest tests/test_labels.py` | склейка дублей, разбор имён, фиксация разбиения, отсутствие утечки между train/val |
|
||
| Имена DICOM-файлов | `python -m pytest tests/test_rename_files.py` | разбор имён, поиск свободного номера при конфликте, отказ от угадывания области, цикл «применить → откатить» |
|
||
| Разметка по экспертной таблице | `python -m pytest tests/test_excel_labels.py` | чтение критериев, голосование по области, перенос на единственное бедро, правила `table`/`union`/`expert`, подключение к обучению |
|
||
| Выбор правила метки | `./run.sh split && ./run.sh compare` | ROC-AUC 0.6726 против 0.6003 по эталону, парная Δ +0.0723 [+0.0541, +0.0905], 5/5 seed'ов в пользу экспертной таблицы |
|
||
| Метрики и порог | `python -m pytest tests/test_preprocess_and_model.py` | подбор порога при дисбалансе, roundtrip чекпоинта, BatchNorm |
|
||
| Веб-интерфейс в браузере | `node tests/browser/ui_check.js` | подсказка о кликабельности строк видна и скрывается на пустом фильтре; кнопка «Открыть» открывает панель; значения панели меняются при переключении строк; панель «О модели» наполняется метриками и словарём |
|
||
| Интерфейс ручной разметки в браузере | `node tests/browser/ui_labeling.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 # работа без доступа к внешним сервисам
|
||
# проверка ручной разметки пишет вердикты, поэтому файл стоит отвести в /tmp:
|
||
DXA_MANUAL_LABELS=/tmp/manual_ui.csv python -m uvicorn src.main:app --port 8123
|
||
node tests/browser/ui_labeling.js
|
||
```
|
||
|
||
Офлайн-режим обеспечен локальными копиями Tailwind и FontAwesome
|
||
(`src/api/static/vendor`, `src/api/static/webfonts`); страница не обращается к CDN.
|
||
|
||
## План доработки
|
||
|
||
- Разметить типы нарушений на уровне снимка и обучить мультилейбл-классификатор.
|
||
- Собрать 500+ исследований для устойчивых метрик и честной валидации.
|
||
- Заменить порог по ширине кадра на калибровку по метаданным аппарата.
|
||
|
||
---
|
||
|
||
## Структура проекта
|
||
|
||
```
|
||
bone_2026/
|
||
├── src/
|
||
│ ├── main.py # FastAPI: маршруты и загрузка модели
|
||
│ ├── dxa/ # действующий модуль оценки качества
|
||
│ │ ├── labels.py # разбор имён, метки, склейка дублей, сплит
|
||
│ │ ├── excel_labels.py # разметка снимков по экспертной таблице
|
||
│ │ ├── manual_labels.py # ручная разметка: хранение вердиктов (/label)
|
||
│ │ ├── review_pack.py # автономный HTML-пакет разметки для врача
|
||
│ │ ├── rename_files.py # приведение имён DICOM к единому виду
|
||
│ │ ├── violations.py # единый словарь типов нарушений (коды, подписи)
|
||
│ │ ├── model_card.py # карточка решения для /api/v1/model и интерфейса
|
||
│ │ ├── compare_labels.py # сравнение источников разметки на одном наборе
|
||
│ │ ├── discriminator.py # проверка вклада содержимого снимка
|
||
│ │ ├── render.py # рендер снимков и контактных листов
|
||
│ │ ├── preprocess.py # DICOM -> тензор (общий для обучения и API)
|
||
│ │ ├── dataset.py # Dataset и DataLoader
|
||
│ │ ├── model.py # сеть, метрики, подбор порога
|
||
│ │ ├── train.py # обучение и отчёт
|
||
│ │ └── inference.py # пакетный инференс, определение области
|
||
│ ├── quality/ # эвристики качества, используются API
|
||
│ └── api/static/ # веб-интерфейс (index.html — анализ, label.html — разметка)
|
||
├── labels/labels_images.csv # разметка снимков: официальная (+ .xlsx)
|
||
├── labels/manual_labels.csv # вердикты специалиста из /label (появляется после правок)
|
||
├── labels/labels_images_table.csv # вариант «только таблица» (то же, что выше)
|
||
├── labels/labels_images_union.csv # вариант «таблица или суффикс имени»
|
||
├── labels/labels_images_expert.csv # эталон для оценки: только снимки с оценкой
|
||
├── labels/split_expert_seed42.json # зафиксированное разбиение (19 исследований)
|
||
├── labels/rename_map.csv # карта переименований файлов (для отката)
|
||
├── assets/img/ # схемы архитектуры и скриншоты интерфейса
|
||
├── assets/labeling.md # как построена разметка и как сравнивались варианты
|
||
├── models/dxa_model.pth # рабочий чекпоинт (+ train_report.md/.json)
|
||
├── tests/ # pytest: метки, сплит, метрики, модель, API
|
||
├── 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 с разметкой (только отчёт о расхождениях) |
|
||
| `--labels-csv` | `labels/labels_images.csv` | Разметка снимков из `./run.sh label`; пустая строка — метки из имён файлов; отсутствующий файл — откат к именам с предупреждением |
|
||
| `--split-file` | — | Зафиксированное разбиение из `./run.sh split`; одинаковый held-out набор для сравнения вариантов разметки |
|
||
| `--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>
|