# 🦴 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`; отчёт удобно приложить к презентации. ## Команда - **Грачев Денис** — разработка - **Грачев Татьяна** — капитан