diff --git a/Dockerfile b/Dockerfile index 80d1d76..a1f35d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,6 +44,17 @@ RUN python -c "import os, torch; p='models/dxa_model.pth'; \ assert 'model_state_dict' in ck, 'checkpoint has no model_state_dict'; \ print('checkpoint ok:', ck.get('backbone'), ck.get('head'))" +# --- Сборка статики для офлайн-работы --- +# Веб-интерфейс не должен зависеть от CDN: Tailwind и FontAwesome лежат в +# src/api/static/vendor и src/api/static/webfonts (см. index.html). Сборка +# проверяет их наличие, чтобы образ не остался без оформления. +RUN python -c "import os; \ + files=['src/api/static/vendor/tailwind.js','src/api/static/vendor/fontawesome.css', \ + 'src/api/static/webfonts/fa-solid-900.woff2']; \ + missing=[f for f in files if not os.path.exists(f)]; \ + assert not missing, f'missing frontend assets: {missing}'; \ + print('frontend assets ok')" + ENV PYTHONUNBUFFERED=1 \ PYTHONPATH=/app \ DXA_MODEL_PATH=/app/models/dxa_model.pth diff --git a/QWEN.md b/QWEN.md index ca8cc05..d4a7b50 100644 --- a/QWEN.md +++ b/QWEN.md @@ -54,21 +54,26 @@ src/dxa/ | Порог по логиту, подбор по F1 | При 15 % нарушений порог 0.5 даёт нулевой recall; вероятности насыщаются | | Аугментация выключена по умолчанию | Яркость и положение сами являются признаками качества: AUC 0.87 → 0.56 | | Область по ширине кадра | Позвоночник 300 px, бедро 280 px; 99/99 для позвоночника | +| Панель деталей показывает измерения, а не вердикты | Эвристики `detailed_assessment` не калиброваны: `motion_detected`/`any_detected` истинны почти всегда, ROI-отступы срабатывают для 227/252 снимков | -### Метрики (валидация, разбиение по исследованиям) +### Проверка вклада модели -- ROC-AUC по 5 разбиениям: **0.76 ± 0.08** (0.64 – 0.84) -- 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 +Не является ли модель просто детектором анатомии (в позвоночнике ~29 % нарушений +против ~4–5 % у бёдер, а область почти однозначно определяется по ширине кадра): -Разбивка по областям обязательна: в позвоночнике ~29 % нарушений против ~4–5 % -у бёдер, а область почти однозначно определяется по ширине кадра, поэтому общий -AUC частично отражает различение области. +```bash +python -m src.dxa.discriminator --model-path models/dxa_model.pth +``` ---- +Результат на чекпоинте `models/dxa_model.pth`: + +| Предиктор | Общий AUC | spine | hip_right | hip_left | +|---|---|---|---|---| +| Модель | 0.822 | 0.838 | 0.895 | 0.948 | +| Правило «позвоночник = нарушение» | 0.724 | 0.500 | 0.500 | 0.500 | + +Модель использует содержимое снимка: внутри областей она даёт 0.84–0.95. +Правило по области внутри области всегда 0.50 (подсказки нет). ## Данные (`dataset_hack/`) @@ -141,12 +146,35 @@ API и CLI используют один код предсказания (`predi ```bash ./run.sh test -python -m pytest tests/ -q # 65 тестов +python -m pytest tests/ -q # 79 тестов ``` -`tests/test_labels.py` — разбор имён, склейка дублей, отсутствие утечки при -разбиении. `tests/test_preprocess_and_model.py` — предобработка, метрики, -подбор порога, контракт модели, BatchNorm при заморозке, roundtrip чекпоинта. +- `tests/test_labels.py` — разбор имён, склейка дублей, отсутствие утечки при + разбиении. +- `tests/test_preprocess_and_model.py` — предобработка, метрики, подбор порога, + контракт модели, BatchNorm при заморозке, roundtrip чекпоинта. +- `tests/test_api_contract.py` — поля ответов, которые читает `dxa-app.js` + (панель деталей ранее показывала прочерки из-за расхождения ключей), + различимость метрик между снимками, валидность PNG-визуализаций, отсутствие + некалиброванных вердиктов в ответе. + +### Проверка веб-интерфейса в браузере + +`tests/browser/*.js` (Node + playwright-core из bundled Browser Use) открывают +интерфейс, загружают DICOM, кликают по строкам таблицы и снимают содержимое +панели деталей. Используется временный профиль Chrome, профиль пользователя не +затрагивается. Требуется запущенный сервер на `127.0.0.1:8123`. + +- `ui_check.js` — значения панели меняются при переключении строк. +- `ui_violation.js` — ветка «нарушение» (бейдж, POOR, HIGH, заключение). +- `ui_offline.js` — страница не обращается к внешним хостам. + +### Офлайн-работа фронтенда + +Tailwind и FontAwesome лежат локально (`src/api/static/vendor`, +`src/api/static/webfonts`), страница не обращается к CDN. Требование методики — +работа без внешних сервисов; CDN-версии ломали оформление в закрытом контуре. +Наличие ассетов проверяется на этапе сборки образа (см. Dockerfile). --- diff --git a/README.md b/README.md index 6fe8145..ec6145d 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,10 @@ |---|---| | Определение анатомической области | Ширина кадра (позвоночник / бедро) + голова области + геометрия яркой зоны | | Бинарная оценка качества | ResNet18 (ImageNet) → линейная голова; порог подобран по F1 на валидации | -| Тип нарушения | Эвристики по изображению: размытие/движение, посторонние включения, геометрия ROI | +| Тип нарушения | Общая категория для снимков с нарушением; детальный тип требует разметки типов на уровне снимка | | Отчёт | XLSX/CSV со столбцами из требований; опционально zip с визуализацией зоны интереса | | API | FastAPI: анализ, детальный анализ, пакетная обработка, экспорт, DICOM SR (текст) | -| Веб-интерфейс | Загрузка DICOM, таблица результатов | +| Веб-интерфейс | Загрузка DICOM, таблица результатов, панель деталей с визуализацией | ## Установка и запуск @@ -193,6 +193,39 @@ curl -X POST http://localhost:8000/api/v1/analyze -F "file=@study/spine.dcm" 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. ## План доработки diff --git a/src/api/static/index.html b/src/api/static/index.html index 548dbf8..0b96ab5 100644 --- a/src/api/static/index.html +++ b/src/api/static/index.html @@ -4,8 +4,13 @@