From 3de6ed5af32441a8592f71001c3c46dddb06e174 Mon Sep 17 00:00:00 2001 From: denis Date: Sun, 27 Sep 2026 22:04:50 +0300 Subject: [PATCH] develop - hack_2026 --- .gitignore | 5 + Dockerfile | 6 +- Dockerfile_cuda => Dockerfile_cuda.txt | 0 QWEN.md | 136 +- README.md | 123 +- assets/labeling.md | 175 +- docker-compose.yml | 2 +- docs/technical-description.html | 202 +- docs/technical-description.pdf | Bin 1107211 -> 1141257 bytes labels/labels_images.csv | 41 +- labels/labels_images.xlsx | Bin 32282 -> 30154 bytes labels/labels_images_expert.csv | 39 +- labels/labels_images_expert.xlsx | Bin 31943 -> 29899 bytes labels/labels_images_table.csv | 41 +- labels/labels_images_table.xlsx | Bin 32282 -> 30155 bytes labels/labels_images_union.csv | 505 +++-- labels/labels_images_union.xlsx | Bin 30341 -> 30234 bytes labels/split_expert_seed42.json | 2 +- models/dxa_model.pth | Bin 44835659 -> 44837323 bytes models/train_report.json | 2642 ++++++++++++++---------- models/train_report.md | 28 +- run.sh | 27 +- src/api/static/js/labeling.js | 494 +++++ src/api/static/label.html | 142 ++ src/dxa/excel_labels.py | 10 +- src/dxa/manual_labels.py | 402 ++++ src/dxa/model_card.py | 55 +- src/dxa/review_pack.py | 841 ++++++++ src/main.py | 266 ++- tests/browser/review_pack.js | 153 ++ tests/browser/ui_labeling.js | 164 ++ tests/test_excel_labels.py | 55 +- tests/test_labeling_api.py | 232 +++ tests/test_manual_labels.py | 229 ++ tests/test_review_pack.py | 212 ++ 35 files changed, 5572 insertions(+), 1657 deletions(-) rename Dockerfile_cuda => Dockerfile_cuda.txt (100%) create mode 100644 src/api/static/js/labeling.js create mode 100644 src/api/static/label.html create mode 100644 src/dxa/manual_labels.py create mode 100644 src/dxa/review_pack.py create mode 100644 tests/browser/review_pack.js create mode 100644 tests/browser/ui_labeling.js create mode 100644 tests/test_labeling_api.py create mode 100644 tests/test_manual_labels.py create mode 100644 tests/test_review_pack.py diff --git a/.gitignore b/.gitignore index cadd626..d4906a5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,8 @@ .vscode/ /.pytest_cache/ /venv/ + +# Автономный HTML-пакет для ручной разметки (снимки внутри, ~7 МБ) — +# генерируется командой `python -m src.dxa.review_pack --out review/doctor_review.html`, +# в репозитории не хранится. +/review/ diff --git a/Dockerfile b/Dockerfile index 6776d9a..94127fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,10 +46,12 @@ RUN python -c "import src.main; print('app import ok')" # --- Проверка статики для офлайн-работы --- # Веб-интерфейс не должен зависеть от CDN: Tailwind и FontAwesome лежат в -# src/api/static/vendor и src/api/static/webfonts (см. index.html). +# src/api/static/vendor и src/api/static/webfonts (см. index.html). Страница +# ручной разметки /label тоже должна попасть в образ целиком. 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']; \ + 'src/api/static/webfonts/fa-solid-900.woff2', \ + 'src/api/static/label.html','src/api/static/js/labeling.js']; \ missing=[f for f in files if not os.path.exists(f)]; \ assert not missing, f'missing frontend assets: {missing}'; \ print('frontend assets ok')" diff --git a/Dockerfile_cuda b/Dockerfile_cuda.txt similarity index 100% rename from Dockerfile_cuda rename to Dockerfile_cuda.txt diff --git a/QWEN.md b/QWEN.md index e1c2ce3..5a65e4e 100644 --- a/QWEN.md +++ b/QWEN.md @@ -37,6 +37,8 @@ src/dxa/ ├── labels.py # имена -> метки, склейка дублей, разбиение, фиксация сплита ├── excel_labels.py # разметка снимков по экспертной таблице (labels_images.csv) +├── manual_labels.py # вердикты специалиста: хранение, наложение, выгрузка (/label) +├── review_pack.py # автономный HTML-пакет разметки для врача (без сервера и сети) ├── rename_files.py # приведение имён DICOM к виду область_NN[_метка] ├── violations.py # единый словарь типов нарушений (коды, подписи, коды SR) ├── model_card.py # карточка решения: разметка, данные, метрики, ограничения @@ -63,14 +65,14 @@ src/dxa/ | Решение | Причина | |---|---| | Единый словарь типов нарушений (`src/dxa/violations.py`) | Коды, подписи и коды SR были в трёх копиях (инференс, `main.py`, `dxa-app.js`) и не знали кодов экспертной таблицы. Теперь подписи отдаёт сервер, фронт копий не держит | -| Метки только из экспертной таблицы (правило `table`): `labels/labels_images.csv`, 77 нарушений | Таблица описывает исследование, но каждая область встречается в нём один раз, поэтому вердикт переносится на снимок однозначно. Правило выбрано измерением: учёт ручных пометок из имён файлов дал ROC-AUC 0.6199 против 0.6764, хуже на всех 5 seed'ах. См. `assets/labeling.md` | -| Склейка побайтных дублей | 544 файла, но 252 уникальных снимка; без склейки снимок попадал в оба класса | +| Метки только из экспертной таблицы (правило `table`): `labels/labels_images.csv`, 76 нарушений | Таблица описывает исследование, но каждая область встречается в нём один раз, поэтому вердикт переносится на снимок однозначно. Правило выбрано измерением: учёт ручных пометок из имён файлов дал ROC-AUC 0.6003 против 0.6726, хуже на всех 5 seed'ах. См. `assets/labeling.md` | +| Склейка побайтных дублей | 482 файла, но 251 уникальный снимок; без склейки снимок попадал в оба класса | | Разбиение по исследованиям, не по снимкам | Исключение утечки: снимки одного исследования в одной части | | Линейный зонд (замороженный backbone) | Полный fine-tune при ~250 снимках переобучается (val AUC → 0.5) | | Порог по логиту, подбор по F1 | При доле нарушений около 30 % порог 0.5 даёт почти нулевой recall; вероятности насыщаются | | Аугментация выключена по умолчанию | Яркость и положение сами являются признаками качества: AUC 0.87 → 0.56 | | Область по ширине кадра | Позвоночник 300 px, бедро 280 px; 99/99 для позвоночника | -| Панель деталей показывает измерения, а не вердикты | Эвристики `detailed_assessment` не калиброваны: `motion_detected`/`any_detected` истинны почти всегда, ROI-отступы срабатывают для 227/252 снимков | +| Панель деталей показывает измерения, а не вердикты | Эвристики `detailed_assessment` не калиброваны: `roi.valid` ложен для всех 251 снимка (краевой отступ срабатывает у 249 из 251), поэтому в панели показываются числовые измерения, а не вердикты «Да/Нет» | ### Проверка вклада модели @@ -86,12 +88,12 @@ python -m src.dxa.discriminator --model-path models/dxa_model.pth | Предиктор | Общий AUC | spine | hip_right | hip_left | |---|---|---|---|---| -| Модель | 0.854 | 0.928 | 0.861 | 0.853 | -| Правило «позвоночник = нарушение» | 0.529 | 0.500 | 0.500 | 0.500 | +| Модель | 0.885 | 0.954 | 0.875 | 0.880 | +| Правило «позвоночник = нарушение» | 0.534 | 0.500 | 0.500 | 0.500 | -Модель использует содержимое снимка: внутри областей она даёт 0.85–0.93. +Модель использует содержимое снимка: внутри областей она даёт 0.88–0.95. Правило по области внутри области всегда 0.50 (подсказки нет). Честная оценка на -held-out — в `assets/labeling.md` §8: ROC-AUC 0.6764 [0.6309, 0.7218] по пяти seed'ам. +held-out — в `assets/labeling.md` §8: ROC-AUC 0.6726 [0.6367, 0.7086] по пяти seed'ам. ## Данные (`dataset_hack/`) @@ -99,7 +101,7 @@ held-out — в `assets/labeling.md` §8: ROC-AUC 0.6764 [0.6309, 0.7218] по ``` dataset_hack/ -├── Для теста/ # bad.dcm, l_hip.dcm, r_hip.dcm, spine.dcm +├── Для теста/ # l_hip_01.dcm, r_hip_01.dcm, r_hip_01_bad.dcm, spine_01.dcm └── НД_для_обучения/ ├── разметка.xlsx # экспертная оценка на уровне ИССЛЕДОВАНИЯ └── Исследования//.../_[_good|_bad].dcm @@ -107,12 +109,13 @@ dataset_hack/ Факты, важные для обучения: -- 544 файла на диске, но **252 уникальных снимка** (по пиксельному содержимому) - на 100 исследований: позвоночник 99, бедро R 79, бедро L 73, 1 с неопределённой +- **482 файла `.dcm` на диске** (было 520: удалены 38 лишних побайтных копий и 10 + файлов `.DS_Store`), но **251 уникальный снимок** (по пиксельному содержимому) + на 100 исследований: позвоночник 99, бедро R 78, бедро L 73, 1 с неопределённой областью. -- Экспертная таблица отмечает нарушения у **74 снимков (29.4 %)**; три снимка +- Экспертная таблица отмечает нарушения у **73 снимков (29.1 %)**; три снимка таблица область не оценивала. -- Рабочая разметка (правило `table`) — **77 нарушений из 252 (30.6 %)**: 74 по +- Рабочая разметка (правило `table`) — **76 нарушений из 251 (30.3 %)**: 73 по таблице плюс 3 снимка без экспертной оценки, помеченных `filename_fallback`. - Дубли не пересекают границы исследований, конфликтов меток при склейке нет. Два побайтных дубля названы по-разному, поэтому область определяется @@ -121,7 +124,9 @@ dataset_hack/ `l_hip`, `r_hip`); инструмент — `src/dxa/rename_files.py`, карта отката — `labels/rename_map.csv`. Суффиксы `_good`/`_bad` проставлялись вручную, в метках **не участвуют** — только как диагностический столбец - `quality_from_filename`: они расходились с оценкой эксперта в 15 случаях из 252. + `quality_from_filename`: они расходились с оценкой эксперта в 62 случаях из 251 + (повторное переименование 2026-09-27 дописало пометки уже после сборки + разметки и с тех пор расхождений стало вчетверо больше). - В DICOM **нет** разметки ROI (ни OverlayData, ни GraphicAnnotationSequence) и пусты теги `Laterality`/`ImageLaterality`, поэтому ни корректность областей, ни сторону бедра нельзя проверить по метаданным. @@ -174,10 +179,10 @@ python -m src.dxa.train --head mlp --freeze-epochs 0 --epochs 30 рассинхронизироваться с обучением, а по файлу видно, на какой разметке он обучен. Рабочий чекпоинт — `models/dxa_model.pth` (правило `table`, seed 42 по умолчанию, -эпоха 39, порог логита −0.4930 → вероятность 0.379; val ROC-AUC 0.6706 при -честной оценке 0.6764 [0.6309, 0.7218] по пяти seed'ам). Это единственный -чекпоинт в репозитории: прежние версии и прогоны сравнения удалены, откатиться -можно только переобучением. +эпоха 57, порог логита −0.1370 → вероятность 0.466; val ROC-AUC 0.6689 при +честной оценке 0.6726 [0.6367, 0.7086] по пяти seed'ам). Переобучен 2026-09-27 +после пересборки разметки. Это единственный чекпоинт в репозитории: прежние +версии и прогоны сравнения удалены, откатиться можно только переобучением. --- @@ -186,6 +191,7 @@ python -m src.dxa.train --head mlp --freeze-epochs 0 --epochs 30 | Метод | Путь | Назначение | |---|---|---| | GET | `/` | Веб-интерфейс | +| GET | `/label` | Интерфейс ручной разметки (подтверждение вердиктов специалистом) | | GET | `/api/v1/health` | Статус, признак загрузки модели и её происхождение (разметка, разбиение, порог, эпоха) | | GET | `/api/v1/model` | Карточка решения: разметка, данные, метрики с интервалами, словарь нарушений, ограничения | | POST | `/api/v1/analyze` | Базовый анализ файла | @@ -193,6 +199,11 @@ python -m src.dxa.train --head mlp --freeze-epochs 0 --epochs 30 | 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` | Сохранить вердикт специалиста в `labels/manual_labels.csv` | +| DELETE | `/api/v1/labeling/verdict` | Снять вердикт и вернуть снимок к построенной разметке | +| GET | `/api/v1/labeling/export` | Выгрузка разметки: `scope=all` годится как `--labels-csv` | Путь к модели — переменная окружения `DXA_MODEL_PATH` (по умолчанию `models/dxa_model.pth`), чтобы контейнер не зависел от рабочего каталога. @@ -205,7 +216,7 @@ API и CLI используют один код предсказания (`predi ```bash ./run.sh test -python -m pytest tests/ -q # 209 тестов +python -m pytest tests/ -q # 272 теста ``` - `tests/test_labels.py` — разбор имён, склейка дублей, отсутствие утечки при @@ -215,7 +226,10 @@ python -m pytest tests/ -q # 209 тестов - `tests/test_excel_labels.py` — разметка по экспертной таблице: чтение критериев, «1 = нарушение», голосование по области, перенос оценки на единственное бедро, три правила метки (`table` / `union` / `expert`) и их - согласованность, подключение к обучению. + согласованность, подключение к обучению, чтение CSV, сохранённого Excel с BOM. +- `tests/test_manual_labels.py` — ручная разметка: проверка вердикта (область, + метка, тип нарушения и его совместимость с областью), хранение и правка, + наложение поверх построенной разметки, подсчёт прогресса и выгрузка. - `tests/test_violations.py` — единый словарь типов: коды и подписи, коды SR, приведение устаревших значений, согласованность с критериями таблицы. - `tests/test_preprocess_and_model.py` — предобработка, метрики, подбор порога, @@ -224,6 +238,13 @@ python -m pytest tests/ -q # 209 тестов (панель деталей ранее показывала прочерки из-за расхождения ключей), различимость метрик между снимками, валидность PNG-визуализаций, отсутствие некалиброванных вердиктов в ответе. +- `tests/test_labeling_api.py` — контракт `/api/v1/labeling/*`: отказ отдавать + файлы вне датасета (обход каталога, не-DICOM), проверка вердикта, выгрузка, + читаемая обучением как `--labels-csv`. +- `tests/test_review_pack.py` — пакет для врача: отпечаток набора, порядок + (расхождения первыми), встроенные данные разбираются как JSON, страница не + ссылается на сеть, слияние вердиктов с построенной разметкой и сообщение о + путях из чужого пакета. ### Проверка веб-интерфейса в браузере @@ -237,6 +258,17 @@ python -m pytest tests/ -q # 209 тестов значения панели меняются при переключении строк; панель «О модели» наполняется метриками и словарём (иначе раздел остался бы пустым каркасом). - `ui_violation.js` — ветка «нарушение» (бейдж, POOR, HIGH, заключение). +- `ui_labeling.js` — интерфейс ручной разметки: список снимков с прогрессом, + расхождения первыми, снимок отрисовывается, вердикт сохраняется и переживает + перезагрузку страницы, фильтр «только расхождения», отсутствие внешних запросов. + Запускать с временным файлом вердиктов: + `DXA_MANUAL_LABELS=/tmp/manual_ui.csv python -m uvicorn src.main:app --port 8123`, + иначе проверка пишет в рабочий `labels/manual_labels.csv`. +- `review_pack.js` — автономный пакет разметки: открывает HTML прямо с диска + (`file://`, сервер не нужен), проверяет отрисовку встроенного снимка, вердикт, + его сохранение после перезагрузки, выгрузку CSV и восстановление прогресса из + этого же файла, а также отсутствие любых сетевых запросов. Запуск: + `PACK=review/doctor_review.html node tests/browser/review_pack.js`. - `ui_offline.js` — страница не обращается к внешним хостам. ### Честность интерфейса @@ -263,9 +295,10 @@ Tailwind и FontAwesome лежат локально (`src/api/static/vendor`, 1. Разметка снимков выведена из таблицы, описывающей исследование: поштучной экспертной оценки снимков в наборе нет. Оценка качества модели упирается в - качество этой разметки, а не только в объём данных. -2. Мало данных: 252 снимка, 77 нарушений; доверительные интервалы широкие - (ROC-AUC 0.6764 [0.6309, 0.7218] по пяти seed'ам). + качество этой разметки, а не только в объём данных. Для поштучной разметки есть + интерфейс `/label` (см. ниже) — им ещё не пользовались. +2. Мало данных: 251 снимок, 76 нарушений; доверительные интервалы широкие + (ROC-AUC 0.6726 [0.6367, 0.7086] по пяти seed'ам). 3. Эталон оценки — та же экспертная таблица, независимой истины нет; сравнение правил разметки частично благоприятствует варианту «только таблица». 4. Тип нарушения определяется эвристиками, а не обученной моделью; 5 снимков @@ -275,6 +308,52 @@ Tailwind и FontAwesome лежат локально (`src/api/static/vendor`, 6. Сторона бедра в 7 исследованиях с единственным снимком не проверяема: теги `Laterality` пусты, оценка взята из единственного заполненного столбца. 7. Порог `SPINE_MIN_WIDTH` привязан к текущему оборудованию. +8. Разметка и датасет уже расходились: повторное переименование 2026-09-27 + дописало суффиксы `_good`/`_bad` после сборки разметки, из-за чего 20 из 252 + путей устарели, 6 снимков получали метку «нарушение» вопреки эксперту и + пайплайн отдавал 251/82 вместо 252/77. Расхождение устранено пересборкой + разметки и переобучением в тот же день; числа в этом файле — уже новые. + Разбор — в `assets/labeling.md` §7. + +## Ручная разметка (`/label`) + +Поштучной экспертной оценки снимков в наборе нет (ограничение №1), а раздел 2.6 +задания просит «автоматическую коррекцию разметки с возможностью подтверждения +специалистом». Поэтому в сервисе есть отдельный интерфейс `/label`: +`src/dxa/manual_labels.py` — хранение вердиктов, эндпоинты `/api/v1/labeling/*` — +список снимков, просмотр и сохранение, страница `src/api/static/label.html` + +`js/labeling.js`. + +Вердикты лежат в `labels/manual_labels.csv` (переменная `DXA_MANUAL_LABELS`) в +формате построенной разметки, поэтому файл читается тем же `load_labels_csv` и +принимается обучением как `--labels-csv`. Выгрузка `scope=all` накладывает ручные +вердикты на построенную разметку и годится как источник меток напрямую. + +Оценка модели в интерфейсе намеренно не показывается: подсказка смещала бы +разметчика, а цель — независимое суждение человека. Список выводит первыми +снимки, где метка разметки расходится с пометкой в имени файла: там ошибка +возможна в любом из источников. + +### Пакет для врача (разметка без сервиса) + +`/label` требует запущенного сервиса и Python, а размечать должен врач — на своей +машине. Поэтому тот же сценарий собирается в **один HTML-файл**: + +```bash +./run.sh review --out review/doctor_review.html # ~7 МБ, 251 снимок +./run.sh review --limit 20 --out review/pilot.html # пилот на выборке +./run.sh review --merge review/doctor.csv --out labels/labels_images_reviewed.csv +``` + +Снимки встроены как data-URI, вердикты лежат в `localStorage` браузера, выгрузка — +CSV в формате `manual_labels.csv` (плюс колонка `pack_id` — отпечаток набора, +чтобы различить пакеты). Файл открывается двойным щелчком, работает без сети и +без установки чего-либо; каталог `review/` в git не хранится. + +Вердикты врача принимаются как есть: `load_verdicts` читает выгрузку, `--merge` +накладывает её на построенную разметку (и сообщает о путях, которых нет в +датасете — признак чужого пакета), а `--labels-csv` с этим файлом годится для +обучения. Оценка модели в пакет не попадает по той же причине, что и в `/label`. ## Удалённый устаревший код @@ -292,6 +371,19 @@ position_validator,universal_scorer,medical_quality}.py`) удалены 2026-09 `__version__`, а `src/quality/__init__.py` — только докстрока. При добавлении новых модулей в `src/quality/` помнить, что пакет больше ничего не импортирует. +`docs/` после удаления появился снова — владелец вернул его под сдачу: там лежат +`technical-description.html` (сдаточный технический документ) и его PDF-версия, +а также презентация команды. Это не материалы README: корневой `README.md` +ссылается только на `assets/`. PDF печатается из HTML тем же Chrome, а не +отдельным конвертером: + +```bash +"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless \ + --disable-gpu --no-pdf-header-footer --user-data-dir=/tmp/chrome-pdf-profile \ + --print-to-pdf="$PWD/docs/technical-description.pdf" \ + "file://$PWD/docs/technical-description.html" +``` + --- ## Docker diff --git a/README.md b/README.md index 5244ede..365c01a 100644 --- a/README.md +++ b/README.md @@ -96,15 +96,16 @@ DICOM ──▶ предобработка ──▶ ResNet18 (замороже из экспертной таблицы командой `./run.sh label` (разбор — в `assets/labeling.md`): в таблице отмечены критерии качества по каждому исследованию, а каждая область встречается в нём ровно один раз, поэтому - вердикт переносится на снимок однозначно. Такой разметки — 77 нарушений из - 252 (30.6 %). Правило выбрано измерением: учёт ручных пометок из имён файлов + вердикт переносится на снимок однозначно. Такой разметки — 76 нарушений из + 251 (30.3 %). Правило выбрано измерением: учёт ручных пометок из имён файлов дал худший результат на held-out наборе, поэтому в метках они не участвуют. Резервный режим `--labels-csv ""` берёт метку из суффикса `_good`/`_bad` и оставлен для совместимости. -3. **Склейка побайтных дублей.** В датасете 544 файла, но 252 уникальных снимка: +3. **Склейка побайтных дублей.** В датасете 482 файла, но 251 уникальный снимок: один и тот же кадр сохранён многократно под разными именами (часть — с меткой, - часть — без). Без склейки одно изображение попадало бы в оба класса. + часть — без). Без склейки одно изображение попадало бы в оба класса. Лишние + байт-идентичные копии удалены. 4. **Разбиение по исследованиям.** Снимки одного исследования не попадают одновременно в train и val — иначе метрики завышаются за счёт утечки. @@ -161,6 +162,7 @@ DICOM ──▶ предобработка ──▶ ResNet18 (замороже | Метод | Путь | Назначение | |---|---|---| | GET | `/` | Веб-интерфейс | +| GET | `/label` | Интерфейс ручной разметки: подтверждение и правка вердиктов специалистом | | GET | `/api/v1/health` | Статус, признак загрузки модели и её происхождение | | GET | `/api/v1/model` | Карточка решения: разметка, данные, метрики с интервалами, словарь нарушений | | POST | `/api/v1/analyze` | Базовый анализ одного файла | @@ -168,6 +170,11 @@ DICOM ──▶ предобработка ──▶ ResNet18 (замороже | 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" @@ -258,27 +265,85 @@ curl -X POST http://localhost:8000/api/v1/analyze -F "file=@study/spine.dcm" - числовые метрики в панели деталей по-прежнему идут под дисклеймером о некалиброванности порогов. +### Ручная разметка (`/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 не хранится: пакет генерируется, а внутри — медицинские +снимки. + --- ## Метрики Метрики зависят от выбранного разбиения по исследованиям, поэтому приводятся с разбросом. Основная оценка — на **фиксированном** разбиении по исследованиям -(обучение 199 снимков / 81 исследование, валидация 53 снимка / 19 исследований, +(обучение 198 снимков / 81 исследование, валидация 53 снимка / 19 исследований, 16 нарушений), пять seed'ов обучения, эталон — вердикт эксперта: | Что измерено | Значение | Как измерено | |---|---|---| -| ROC-AUC | **0.6764** [0.6309, 0.7218] | 5 seed'ов на фиксированном разбиении | -| PR-AUC | 0.4759 [0.4141, 0.5377] | там же; базовый уровень при 30 % нарушений — 0.30 | -| F1 | 0.5676 [0.5270, 0.6082] | там же; порог подобран на той же валидации — смещено вверх | -| ROC-AUC по областям | позвоночник 0.943, бедро R 0.576, бедро L 0.550 | рабочий чекпоинт, собственная валидация | +| 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.854 против 0.529 у правила области | `discriminator` на всём наборе, включая обучающие снимки | +| Модель использует снимок, а не область | AUC 0.885 против 0.534 у правила области | `discriminator` на всём наборе, включая обучающие снимки | Метрики по областям — в `models/train_report.md`, он создаётся при обучении. Разбивка важна, потому что нарушения распределены неравномерно: в позвоночнике -33 из 99, у бёдер 21–22 из 73–79, а область почти однозначно определяется по +33 из 99, у бёдер 20–22 из 73–78, а область почти однозначно определяется по ширине кадра. Поэтому общий AUC частично отражает различение области, а не только распознавание дефекта. @@ -286,18 +351,18 @@ curl -X POST http://localhost:8000/api/v1/analyze -F "file=@study/spine.dcm" Метку можно было строить только из экспертной таблицы или дополнительно учитывать пометки, проставленные вручную в именах файлов (суффикс `_bad`). -Пометки расходились с оценкой эксперта в 15 случаях из 252, поэтому правило +Пометки расходились с оценкой эксперта в 62 случаях из 251, поэтому правило выбиралось измерением: одно разбиение, пять seed'ов, один эталон. | Метрика (эталон) | только таблица | с суффиксами имён | |---|---|---| -| ROC-AUC | **0.6764** [0.6309, 0.7218] | 0.6199 [0.5840, 0.6559] | -| PR-AUC | **0.4759** [0.4141, 0.5377] | 0.4046 [0.3702, 0.4391] | -| F1 | **0.5676** [0.5270, 0.6082] | 0.5426 [0.5073, 0.5778] | -| Recall / Precision | 0.700 / 0.486 | 0.863 / 0.404 | +| 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.0564** -[+0.0403, +0.0725], PR-AUC **+0.0713** [+0.0398, +0.1028] — знаки `+++++`, то +Парная разница (только таблица − с суффиксами): ROC-AUC **+0.0723** +[+0.0541, +0.0905], PR-AUC **+0.0889** [+0.0500, +0.1277] — знаки `+++++`, то есть преимущество на всех пяти seed'ах. **Принято правило «только экспертная таблица».** Пометки в именах файлов @@ -322,12 +387,13 @@ curl -X POST http://localhost:8000/api/v1/analyze -F "file=@study/spine.dcm" 1. **Разметка выведена из оценки исследования.** Экспертная таблица описывает исследование, а не снимок; перенос однозначен (область встречается один раз), но поштучной экспертной оценки снимков в наборе нет. Происхождение каждой - строки зафиксировано в `labels/labels_images.csv`. Разбор — в + строки зафиксировано в `labels/labels_images.csv`. Для поштучной разметки есть + интерфейс `/label` (см. «Веб-интерфейс»), им ещё не пользовались. Разбор — в `assets/labeling.md`. -2. **Мало данных.** 252 уникальных снимка, 77 нарушений. Доверительные интервалы +2. **Мало данных.** 251 уникальный снимок, 76 нарушений. Доверительные интервалы широкие; оценка на закрытом наборе может отличаться. 3. **Тип нарушения определяется эвристиками, а не обученной моделью.** Для - честного мультикласса нужна разметка типов на уровне снимка. + честного мультикласса нужна разметка типов на уровне снимка — её даёт `/label`. 4. **Область определяется по размеру кадра.** Признак безошибочно работает на этом оборудовании (99/99 для позвоночника), но при смене аппарата порог `SPINE_MIN_WIDTH` потребует калибровки. @@ -350,15 +416,18 @@ curl -X POST http://localhost:8000/api/v1/analyze -F "file=@study/spine.dcm" | Проверка | Команда | Результат | |---|---|---| -| Модель использует снимок, а не только область | `python -m src.dxa.discriminator` | AUC 0.854 против 0.529 у правила «позвоночник = нарушение»; внутри областей у модели 0.85–0.93, у правила 0.50 | +| Модель использует снимок, а не только область | `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.6764 против 0.6199 по эталону, парная Δ +0.0564 [+0.0403, +0.0725], 5/5 seed'ов в пользу экспертной таблицы | +| Выбор правила метки | `./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` | ноль внешних запросов, стили и иконки на месте | Браузерные проверки требуют запущенного сервера: @@ -367,6 +436,9 @@ curl -X POST http://localhost:8000/api/v1/analyze -F "file=@study/spine.dcm" 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 @@ -389,6 +461,8 @@ bone_2026/ │ ├── 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 и интерфейса @@ -401,8 +475,9 @@ bone_2026/ │ │ ├── train.py # обучение и отчёт │ │ └── inference.py # пакетный инференс, определение области │ ├── quality/ # эвристики качества, используются API -│ └── api/static/ # веб-интерфейс +│ └── 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 # эталон для оценки: только снимки с оценкой diff --git a/assets/labeling.md b/assets/labeling.md index 7bdf25b..b6cfbcc 100644 --- a/assets/labeling.md +++ b/assets/labeling.md @@ -43,7 +43,7 @@ | Шаг | Что делается | Почему так | |---|---|---| | Ключ склейки | имя каталога исследования | таблица ссылается на каталог (`2.25…`), а в DICOM лежит другой идентификатор (`1.2.643…`); соответствие каталог → тег 100/100 | -| Склейка дублей | по хешу пиксельных данных | 544 файла — это 252 уникальных снимка | +| Склейка дублей | по хешу пиксельных данных | 478 файлов — это 251 уникальный снимок | | Область снимка | голосование по именам файлов группы | один снимок назван и как позвоночник, и как бедро; при равенстве голосов область остаётся неопределённой (такой снимок один) | | Вердикт | оценка области переносится на её снимок | **каждая область встречается в исследовании ровно один раз**, поэтому не нужно решать, какой из нескольких снимков «плохой» | | Сторона бедра | при единственном снимке бедра берётся единственный заполненный столбец | в 71 из 72 исследований с двумя бёдрами столбцы совпадают с именами файлов; в 7 исследованиях с одним снимком заполнена противоположная сторона. Факт переноса фиксируется флагом `laterality_mirrored`; теги `Laterality` в DICOM пусты | @@ -53,8 +53,8 @@ Метка могла строиться двумя способами: только из таблицы или с добавлением пометок, которые вручную проставлялись в именах файлов (суффикс `_bad`). Пометки -в именах оказались ненадёжными: они расходились с оценкой эксперта в **15 случаях -из 252**. Выбор сделан измерением, а не по вкусу. +в именах оказались ненадёжными: они расходились с оценкой эксперта в **62 случаях +из 251**. Выбор сделан измерением, а не по вкусу. **Постановка.** Разбиение по исследованиям зафиксировано один раз, оба варианта обучены пятью seed'ами на нём, оценены по одному эталону — вердикту эксперта на @@ -67,25 +67,25 @@ | Метрика (эталон) | только таблица | таблица или суффикс `_bad` | |---|---|---| -| ROC-AUC | **0.6764** [0.6309, 0.7218] | 0.6199 [0.5840, 0.6559] | -| PR-AUC | **0.4759** [0.4141, 0.5377] | 0.4046 [0.3702, 0.4391] | -| F1 | **0.5676** [0.5270, 0.6082] | 0.5426 [0.5073, 0.5778] | -| Recall | 0.7000 | 0.8625 | -| Precision | 0.4857 | 0.4043 | +| 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 | 0.7125 | 0.7875 | +| Precision | 0.4716 | 0.4087 | Парная разница («только таблица» − «с суффиксами»): | Метрика | Δ, среднее [95 % ДИ] | Знаки по seed'ам | |---|---|---| -| ROC-AUC | **+0.0564** [+0.0403, +0.0725] | `+++++` | -| PR-AUC | **+0.0713** [+0.0398, +0.1028] | `+++++` | -| F1 | +0.0251 [−0.0056, +0.0558] | `+++-+` | -| Precision | +0.0815 [+0.0590, +0.1039] | `+++++` | -| Recall | −0.1625 [−0.2715, −0.0535] | `--0--` | +| ROC-AUC | **+0.0723** [+0.0541, +0.0905] | `+++++` | +| PR-AUC | **+0.0889** [+0.0500, +0.1277] | `+++++` | +| F1 | +0.0205 [−0.0182, +0.0592] | `+-+-+` | +| Precision | +0.0629 [−0.0133, +0.1392] | `+++-+` | +| Recall | −0.0750 [−0.1931, +0.0431] | `0---0` | **Решение: принято правило «только экспертная таблица».** Дополнительные пометки из имён файлов ухудшали согласие модели с экспертом на невиданных -исследованиях: вариант с ними чаще срабатывал (recall 0.86), но за счёт +исследованиях: вариант с ними чаще срабатывал (recall 0.79), но за счёт точности, а по беспороговым метрикам проигрывал на всех пяти seed'ах. Оговорка: эталон — та же экспертная таблица, поэтому вариант, обучавшийся @@ -107,8 +107,9 @@ | `labeling_error` | Ошибка разметки | позвоночник | критерии методики | | `unspecified` | Нарушение без уточнения | любая | служебный | -Распределение в разметке: `rotation` 36, `artifact` 17, `axis_deviation` 10, -`roi_incorrect` 7, `positioning` 6, `unspecified` 5. +Распределение в разметке: `rotation` 35, `artifact` 17, `axis_deviation` 10, +`roi_incorrect` 7, `positioning` 6, `unspecified` 5. Всего 80 критериев на 76 +нарушений: у части снимков таблица отмечает по два критерия. Словарь один на всё решение (`src/dxa/violations.py`): коды используют инференс, отчёт DICOM SR и веб-интерфейс, подписи отдаёт сервер, копий в JavaScript нет. @@ -120,8 +121,8 @@ | | позвоночник | бедро R | бедро L | неопред. | всего | |---|---|---|---|---|---| | качественных | 66 | 58 | 51 | 0 | 175 | -| с нарушением | 33 | 21 | 22 | 1 | 77 | -| **итого** | **99** | **79** | **73** | **1** | **252** | +| с нарушением | 33 | 20 | 22 | 1 | 76 | +| **итого** | **99** | **78** | **73** | **1** | **251** | Помимо метки в CSV сохранено происхождение: `quality_from_excel` (вердикт эксперта), `quality_from_filename` (пометка из имени файла), `sources_conflict`, @@ -130,7 +131,7 @@ Рядом лежат варианты для воспроизведения сравнения: `labels_images_table.csv`, `labels_images_union.csv`, `labels_images_expert.csv` -(эталон: только снимки с экспертной оценкой, 249 строк) и +(эталон: только снимки с экспертной оценкой, 248 строк) и `split_expert_seed42.json` (зафиксированное разбиение). ## 7. Гигиена данных: имена файлов @@ -144,25 +145,56 @@ проверено, что оно на них не влияет: набор из 252 пиксельных групп идентичен до и после, разметка не изменилась ни в одной строке. Суффиксы `_good`/`_bad` сохранены как были и в метках не участвуют — только как диагностический столбец. +Числа в этих двух абзацах — состояние на момент проверки; актуальное состояние +набора и разметки приведено ниже. + +### Расхождение разметки с датасетом и его устранение (2026-09-27) + +К описанному проходу добавился более поздний: 2026-09-27 около 02:02 суффиксы +`_good`/`_bad` дописаны ещё 54 файлам, то есть **после** сборки разметки +(2026-09-26 22:35). `apply_excel_labels` сопоставляет метки строго по +`path_to_image`, поэтому во всех четырёх файлах разметки (`labels_images.csv`, +`_table`, `_union`, `_expert`) 20 строк стали ссылаться на имена, которых больше +нет. Последствие: эти 20 снимков теряли экспертную метку и падали на пометку из +имени файла, у 6 из них метка инвертировалась 0 → 1 (вердикт эксперта — «годное», +файл назван `..._bad.dcm`), и пайплайн отдавал 251 снимок / 82 нарушения вместо +записанных выше 252 / 77. Расхождение нашли сравнением меток с именами файлов; +тесты падали на числах (`assert 251 == 252`), то есть оно было замечено, а не +осталось незамеченным. + +Устранено пересборкой: `./run.sh label` и три варианта правила, `./run.sh split`, +затем переобучение `./run.sh train`. Актуальное состояние — 251 снимок / 76 +нарушений, все пути разметки существуют, расхождений с датасетом нет. Пометки в +именах при этом расходятся с вердиктом эксперта в **62 случаях из 251** (было 15 +из 252): после второго переименования пометок стало больше, и они чаще спорят с +таблицей. На метку это не влияет — правило `table` их не использует (см. §4), — +но именно эти 62 снимка имеет смысл разобрать первыми в интерфейсе ручной +разметки (см. §10). + +Чистка датасета в тот же день: удалены 38 лишних байт-идентичных копий `.dcm` +внутри `НД_для_обучения` и 10 файлов `.DS_Store`. На разметку и на набор +пиксельных групп это не влияет: групп 251, все совпадают с `path_to_image`. На +диске 482 `.dcm` из 494 файлов. У каждого удалённого файла остался байт-идентичный +близнец, поэтому потеряно только имя, а не содержимое. ## 8. Оценка качества модели -Разбиение по исследованиям (по умолчанию seed 42): обучение 199 снимков / +Разбиение по исследованиям (по умолчанию seed 42): обучение 198 снимков / 81 исследование, валидация 53 снимка / 19 исследований, 16 нарушений. | Метрика | Значение | Как получено | |---|---|---| -| ROC-AUC | **0.6764** [0.6309, 0.7218] | 5 seed'ов на фиксированном разбиении, эталон — вердикт эксперта | -| PR-AUC | 0.4759 [0.4141, 0.5377] | там же | -| F1 | 0.5676 [0.5270, 0.6082] | там же; порог подобран по F1 на валидации, поэтому смещён вверх | -| ROC-AUC по областям | позвоночник 0.943, бедро R 0.576, бедро L 0.550 | рабочий чекпоинт, его собственная валидация | +| ROC-AUC | **0.6726** [0.6367, 0.7086] | 5 seed'ов на фиксированном разбиении, эталон — вердикт эксперта | +| PR-AUC | 0.4753 [0.4188, 0.5318] | там же | +| F1 | 0.5559 [0.5212, 0.5906] | там же; порог подобран по F1 на валидации, поэтому смещён вверх | +| ROC-AUC по областям | позвоночник 0.929, бедро R 0.606, бедро L 0.567 | рабочий чекпоинт, его собственная валидация | -Рабочий чекпоинт — обычный прогон с seed по умолчанию (эпоха 39, порог логита -−0.4930 → вероятность 0.379), его собственная валидационная ROC-AUC 0.6706 +Рабочий чекпоинт — обычный прогон с seed по умолчанию (эпоха 57, порог логита +−0.1370 → вероятность 0.466), его собственная валидационная ROC-AUC 0.6689 близка к среднему по seed'ам, то есть результат не отобран по удачности. Проверка, что модель смотрит на снимок, а не угадывает анатомию: внутри областей -она даёт AUC 0.85–0.93, правило «позвоночник значит нарушение» — ровно 0.50. +она даёт AUC 0.88–0.95, правило «позвоночник значит нарушение» — ровно 0.50. Числа считаются на всём наборе, включая обучающие снимки, поэтому смещены вверх и отвечают на вопрос «есть ли вклад содержимого», а не «каково качество на новых данных». @@ -172,7 +204,7 @@ 1. **Разметка унаследована от исследования.** Таблица оценивает исследование, а не снимок; перенос однозначен, потому что область встречается один раз, но исходная оценка всё равно не поштучная. -2. **Мало данных:** 252 снимка, 77 нарушений. Интервалы широкие. +2. **Мало данных:** 251 снимок, 76 нарушений. Интервалы широкие. 3. **Эталон — та же таблица.** Независимой истины нет; вариант, обучавшийся на таблице, в сравнении в выигрышном положении. 4. **Тип нарушения — эвристика**, а не вывод модели: 5 снимков имеют только @@ -182,8 +214,83 @@ 6. **Сторона бедра в 7 исследованиях не проверяема:** теги латеральности пусты. 7. **Корректность областей интереса наследуется из таблицы:** разметки ROI в DICOM нет, сравнить её напрямую не с чем. +8. **Пометки в именах файлов спорят с таблицей в 62 случаях из 251.** На метку + это не влияет (правило `table`), но означает, что один из двух источников + ошибается почти в четверти набора. Разбирать их поштучно можно в `/label`; для + выбора правила измерения в §8 такие снимки не использовались. +9. **Поштучной разметки всё ещё нет.** Интерфейс `/label` (§10) её даёт, но им + ещё не пользовались: пока набор размечен на уровне исследования. -## 10. Отрицательный результат: локальная vision-модель +## 10. Ручная разметка: интерфейс `/label` + +Поштучной экспертной оценки снимков в наборе нет — это ограничение №1, и +автоматически его не закрыть: локальная vision-модель оказалась непригодна (§11), +а экспертная таблица описывает исследование. Поэтому в сервисе есть интерфейс +ручной разметки — раздел 2.6 задания прямо просит «автоматическую коррекцию +разметки с возможностью подтверждения специалистом». + +```bash +./run.sh serve # http://localhost:8000/label +``` + +Что он делает: + +* показывает снимок целиком (PNG через `/api/v1/labeling/image`) без наложений, + чтобы разметчик судил по изображению, а не по подсказке алгоритма; +* отдаёт список снимков с текущей меткой и её источником (`table`, `filename` или + `manual`), причём снимки с расхождением метки и пометки в имени файла идут + первыми — именно там один из источников ошибается (см. §9, п. 8); +* сохраняет вердикт: анатомическая область, «годное / нарушение», тип нарушения из + `violations.py` и комментарий. Тип нарушения проверяется на совместимость с + областью (ротация — критерий бедра), у качественного снимка типа быть не может, + а опечатка в коде не превращается молча в `unspecified`; +* пишет вердикты в `labels/manual_labels.csv`. Формат тот же, что у построенной + разметки, поэтому файл читается `load_labels_csv` и принимается обучением как + `--labels-csv`; +* выгружает `scope=all` — весь набор с наложенными ручными вердиктами (готовый + источник меток для `./run.sh train`) — или `scope=reviewed` — только разобранные + снимки, чтобы сверить их с построенной разметкой. + +Оценка модели в интерфейсе намеренно не показывается: разметчик, видя подсказку, +соглашался бы с ней, и поштучная разметка теряла бы ценность независимого +суждения. Прогресс по областям и число расхождений показаны, чтобы работу можно +было вести частями и прерывать. + +Горячие клавиши: `g` — годное, `b` — нарушение, `Enter` — сохранить и перейти к +следующему, `d` — снять вердикт, `j`/`k` — навигация по списку. + +Инструмент не заменяет эксперта: он только фиксирует суждение специалиста в +формате, который пайплайн уже умеет читать. + +### Передача врачу: автономный пакет + +`/label` требует запущенного сервиса и Python, а размечать должен врач — обычно на +своей машине, вне сети и без установки чего-либо. Поэтому тот же сценарий +собирается в один HTML-файл со встроенными снимками (`src/dxa/review_pack.py`): + +```bash +python -m src.dxa.review_pack --out review/doctor_review.html # 251 снимок, ~7 МБ +python -m src.dxa.review_pack --merge review/doctor.csv \ + --out labels/labels_images_reviewed.csv # наложить вердикты врача +``` + +Страница не делает ни одного сетевого запроса (проверяется тестом), вердикты +хранит браузер, выгрузка — CSV в формате `manual_labels.csv` с дополнительной +колонкой `pack_id` (отпечаток набора: по нему видно, что файл вернулся из того же +пакета). Модель в пакет не попадает: если показать разметчику оценку алгоритма, +он будет с ней соглашаться, и независимого суждения не получится. + +**Что стоит размечать.** Порядок в пакете и в интерфейсе — как в §7: сперва 62 +снимка, где метка разметки расходится с пометкой в имени файла (там ошибается +один из двух источников). Но одних расхождений мало: они отобраны по признаку, +который сам может быть смещён. Чтобы получить **независимую** оценку качества, +нужен ещё случайный подсчёт — если врач разметит, например, случайные 50 снимков +из 251, по ним можно будет измерить ROC-AUC и F1 модели на человеческом суждении, +а не на той же экспертной таблице, из которой выведена разметка (ограничение 3 в +§9). Такой замер в проекте ещё не делался: ни один снимок пока не размечен +поштучно. + +## 11. Отрицательный результат: локальная vision-модель Планировалась визуальная разметка локальной vision-моделью (9 млрд параметров, офлайн), чтобы не зависеть от таблицы. На калибровке по 14 снимкам, из которых 8 @@ -196,7 +303,7 @@ контактных листов остались в `src/dxa/render.py` — они полезны для выборочной ручной проверки. -## 11. Воспроизведение +## 12. Воспроизведение ```bash ./run.sh label # разметка по экспертной таблице @@ -206,9 +313,11 @@ python -m src.dxa.excel_labels --label-rule union --out-name labels_images_union python -m src.dxa.render --by-region --out dataset_hack/_preview ``` -## 12. Что дальше +## 13. Что дальше -- Поштучная разметка снимков специалистом — снимет ограничение №1. -- Мультилейбл по типам нарушений вместо эвристики. +- Поштучная разметка снимков специалистом через `/label` (§10) — снимет + ограничение №1 и позволит перемерить качество на независимом суждении. +- Мультилейбл по типам нарушений вместо эвристики: в интерфейсе тип уже + проставляется вручную, но модель его не предсказывает. - Больше исследований (500+), чтобы сузить интервалы. - Калибровка порога определения области под конкретное оборудование. diff --git a/docker-compose.yml b/docker-compose.yml index 0fec134..5b1cc62 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,7 @@ services: image: dxa-quality:cuda build: context: . - dockerfile: Dockerfile_cuda + dockerfile: Dockerfile_cuda.txt ports: - "8000:8000" restart: unless-stopped diff --git a/docs/technical-description.html b/docs/technical-description.html index 72e9c16..1c9c51e 100644 --- a/docs/technical-description.html +++ b/docs/technical-description.html @@ -67,9 +67,9 @@ Дата документа27 сентября 2026 г. КомандаГрачев Денис — разработка; Грачев Татьяна — капитан Версия решения1.0.0 (src/__init__.py) - Рабочий чекпоинтmodels/dxa_model.pth: ResNet18 + линейная голова, разметка table, seed 42, эпоха 39, порог логита −0.4930 (вероятность 0.379) - Ключевые метрикиROC-AUC 0.6764 [0.6309, 0.7218], F1 0.5676 [0.5270, 0.6082] — пять seed'ов на фиксированном разбиении по исследованиям - Время обработки0.021 с на изображение (медиана, CPU), 548 файлов за 12.1 с; требование «≤ 3 мин на исследование» выполняется с запасом + Рабочий чекпоинтmodels/dxa_model.pth: ResNet18 + линейная голова, разметка table, seed 42, эпоха 57, порог логита −0.1370 (вероятность 0.466) + Ключевые метрикиROC-AUC 0.6726 [0.6367, 0.7086], F1 0.5559 [0.5212, 0.5906] — пять seed'ов на фиксированном разбиении по исследованиям + Время обработки0.022 с на изображение (медиана, CPU), 482 файла за 13.9 с; требование «≤ 3 мин на исследование» выполняется с запасом @@ -144,7 +144,7 @@ Разд. 9 (состав столбцов проверен на реальном прогоне) Время обработки одного исследования ≤ 3 мин Один снимок — один прямой проход сети 224×224, без итеративных процедур - Разд. 13: 0.021 с медиана на CPU, запас более чем трёхкратный + Разд. 13: 0.022 с медиана на CPU, запас более чем трёхкратный Отсутствие необработанных исключений; ошибки фиксируются в отчёте Ошибка на файле не прерывает пакет: строка получает processing_status = Failure: … Разд. 16 @@ -155,8 +155,11 @@ Обход каталога, лог прогресса, XLSX/CSV, опциональный zip с визуализацией Разд. 9, 13 API для пакетной обработки тестового набора - FastAPI: восемь маршрутов, включая /api/v1/batch и /api/v1/export + FastAPI: четырнадцать маршрутов, включая /api/v1/batch, /api/v1/export и маршруты ручной разметки /api/v1/labeling/* Разд. 10 + Дополнительно (п. 2.6): коррекция разметки с возможностью подтверждения специалистом + Интерфейс /label: снимок целиком, текущая метка с указанием источника, вердикт специалиста сохраняется в labels/manual_labels.csv и принимается обучением как --labels-csv + Разд. 11.1; tests/test_manual_labels.py, tests/test_labeling_api.py, tests/browser/ui_labeling.js Обязательная контейнеризация, скрипт сборки и запуска в Linux Dockerfile (python:3.11-slim, чекпоинт внутри образа), Dockerfile_cuda для GPU, docker-compose.yml на оба случая, run.sh Разд. 14; сборка и запуск проверены, см. 14 @@ -224,7 +227,7 @@ Параметры предобработки хранятся в чекпоинте и берутся из него, а не из кода по умолчанию.
  • Оценка качества. Один прямой проход сети даёт логит. Решение принимается по логиту: quality_class = 1, если логит выше порога. Порог подобран по F1 на валидации и - сохранён в чекпоинте (текущее значение −0.4930, что соответствует вероятности 0.379).
  • + сохранён в чекпоинте (текущее значение −0.1370, что соответствует вероятности 0.466).
  • Анатомическая область. Определяется независимо от модели, по геометрии кадра (разд. 7), с оценкой уверенности.
  • Измерения и тип нарушения. Для снимков с нарушением вычисляются числовые признаки снимка, @@ -242,12 +245,12 @@ - - + + - - - + + +
    ПоказательЗначениеПояснение
    Файлов на диске544в обучающем наборе (dataset_hack/НД_для_обучения)
    Уникальных снимков252по пиксельному содержимому; 292 файла — побайтные дубли
    Файлов на диске478DICOM в обучающем наборе (dataset_hack/НД_для_обучения); лишние побайтные копии удалены
    Уникальных снимков251по пиксельному содержимому; 227 файлов — дубли одного и того же кадра
    Исследований100разбиение выполняется по исследованиям, а не по снимкам
    Позвоночник / бедро правое / бедро левое / не определено99 / 79 / 73 / 1голосование по именам файлов после склейки дублей
    Нарушений по экспертной таблице74 (29.4 %)три снимка таблица не оценивала
    Нарушений в рабочей разметке77 (30.6 %)74 по таблице плюс 3 снимка с пометкой в имени файла
    Позвоночник / бедро правое / бедро левое / не определено99 / 78 / 73 / 1голосование по именам файлов после склейки дублей
    Нарушений по экспертной таблице73 (29.1 %)три снимка таблица не оценивала
    Нарушений в рабочей разметке76 (30.3 %)73 по таблице плюс 3 снимка с пометкой в имени файла

    Дубли не пересекают границы исследований, конфликтов меток при склейке не возникает. Два побайтных @@ -267,7 +270,7 @@

    Возможны были два правила: учитывать только экспертную таблицу либо дополнительно учитывать пометки _good/_bad, проставленные вручную в именах файлов. Пометки -расходились с экспертом в 15 случаях из 252, поэтому правило выбиралось измерением: одно +расходились с экспертом в 62 случаях из 251, поэтому правило выбиралось измерением: одно зафиксированное разбиение, пять seed'ов обучения, один эталон (табл. в разд. 12.3). Выбрано правило «только экспертная таблица».

    @@ -275,7 +278,7 @@ - +
    ЧастьСнимковИсследованийНарушенийФайл
    Обучение1998161labels/split_expert_seed42.json
    Обучение1988160labels/split_expert_seed42.json
    Валидация531916
    @@ -383,9 +386,9 @@ backbone заморожен и обучается только голова на Аргумент --min-recall позволяет вместо этого взять максимальный порог с recall не ниже заданного.

    -

    Рабочий чекпоинт. Лучшая эпоха — 39 из прогона в 64 эпохи (обучение остановлено по терпению), -порог логита −0.4930 (вероятность 0.379). Метрики этой эпохи — в разделе 12, честная оценка варианта на -пяти seed'ах — ROC-AUC 0.6764.

    +

    Рабочий чекпоинт. Лучшая эпоха — 57 из прогона в 82 эпохи (обучение остановлено по терпению), +порог логита −0.1370 (вероятность 0.466). Метрики этой эпохи — в разделе 12, честная оценка варианта на +пяти seed'ах — ROC-AUC 0.6726.

    6.3. Чекпоинт и отчёты

    @@ -437,17 +440,17 @@ backbone заморожен и обучается только голова на

    Если ширина кадра неизвестна, используется предсказание головы, а затем форма яркой области (bbox_aspect и symmetry).

    -

    Точность. Сопоставление с областью из рабочей разметки (252 снимка, раздел 4):

    +

    Точность. Сопоставление с областью из рабочей разметки (251 снимок, раздел 4):

    - - + +
    Что проверялосьРезультат
    Позвоночник99 / 99
    Сторона бедра (правое / левое)135 / 152 (88.8 %)
    Итого по всем областям234 / 251 (93.2 %)
    Сторона бедра (правое / левое)134 / 151 (88.7 %)
    Итого по всем областям233 / 250 (93.2 %)

    Различение позвоночника и бедра по ширине кадра работает безошибочно, а сторона бедра определяется -менее надёжно: перевес светимости путает левое и правое в 17 случаях из 152. Сторона не проверялась по +менее надёжно: перевес светимости путает левое и правое в 17 случаях из 151. Сторона не проверялась по тегам DICOM — Laterality в наборе пуст, — поэтому это ограничение (раздел 17, п. 7), а не измеренная ошибка модели.

    @@ -490,12 +493,12 @@ acceptable) с флагом FINAL, нарушение — с фла

    Экспертная таблица кодирует не все девять кодов, а пять: positioning, axis_deviation, artifact, rotation, roi_incorrect. -Распределение в рабочей разметке (в четырёх из 77 нарушений указано по два критерия, поэтому сумма -больше 77):

    +Распределение в рабочей разметке (в четырёх из 76 нарушений указано по два критерия, поэтому сумма +больше 76):

    - + @@ -519,8 +522,8 @@ acceptable) с флагом FINAL, нарушение — с фла
    На текущем чекпоинте эта ветка практически вырождена. Пороги motion_threshold и artifact_threshold в вызов не передаются, поэтому действуют значения по умолчанию (0.0 и 1.0), которых признаки достичь не могут, а условие ротации -(bbox_aspect вне диапазона 0.4–3.0) на наборе не срабатывает. Прогон по всем 548 файлам -даёт одинаковый результат: все 341 решение с нарушением помечены unspecified. Для +(bbox_aspect вне диапазона 0.4–3.0) на наборе не срабатывает. Прогон по всем 482 файлам +даёт одинаковый результат: все 206 решений с нарушением помечены unspecified. Для содержательного типа нужна разметка типов на уровне снимка и обученный мультилейбл-классификатор (раздел 18), поэтому в ответе API тип всегда идёт с флагом violation_type_is_heuristic = true.
    @@ -651,50 +654,92 @@ acceptable) с флагом FINAL, нарушение — с фла состав данных, словарь нарушений и список ограничений. Данные приходят из /api/v1/model. +

    11.1. Ручная разметка (/label)

    + +

    Поштучной экспертной оценки снимков в наборе нет — это ограничение 1 в разд. 17, и автоматически его +закрыть нечем: локальная vision-модель оказалась непригодна как разметчик (отрицательный результат +описан в assets/labeling.md §11). Поэтому реализован отдельный интерфейс ручной разметки, который +отвечает пункту 2.6 задания — «автоматическая коррекция разметки с возможностью подтверждения +специалистом».

    + +

    Страница доступна по адресу /label и работает на тех же офлайн-ассетах, что и основная. +Она показывает снимок целиком (PNG через /api/v1/labeling/image, без наложений), его +текущую метку и источник этой метки: table (экспертная таблица), +filename (пометка в имени файла) или manual (вердикт специалиста). Снимки, где +метка разметки расходится с пометкой в имени файла, выводятся первыми: именно там один из источников +ошибается, и таких снимков в наборе 62 из 251.

    + +

    Специалист подтверждает вердикт или ставит свой: анатомическая область, «годное / нарушение», тип +нарушения из словаря (разд. 8) и комментарий. Вердикт проверяется на согласованность: тип нарушения +обязан относиться к выбранной области (ротация — критерий бедра), у качественного снимка типа быть не +может, а неизвестный код отклоняется, а не подменяется на «не уточнён». Есть горячие клавиши +(g — годное, b — нарушение, Enter — сохранить и перейти к +следующему) и прогресс по областям, чтобы работу можно было вести частями.

    + +

    Вердикты сохраняются в labels/manual_labels.csv в том же формате, что и построенная +разметка, поэтому файл читается тем же загрузчиком и принимается обучением как +--labels-csv. Выгрузка scope=all отдаёт весь набор с наложенными ручными +вердиктами (готовый источник меток), scope=reviewed — только разобранные снимки, чтобы +сверить их с построенной разметкой.

    + +

    Разметка без сервиса. Интерфейс /label требует запущенного сервиса, а +размечать должен специалист — как правило, на своей машине, вне сети и без установки чего-либо. +Поэтому тот же сценарий упаковывается в один автономный HTML-файл +(src/dxa/review_pack.py, около 7 МБ на 251 снимок): изображения встроены как +data-URI, вердикты хранятся в браузере, а кнопка «Выгрузить CSV» отдаёт файл, который принимается +обучением как есть. Страница не делает ни одного сетевого запроса, и это проверяется автоматически +(tests/browser/review_pack.js). Вердикты специалиста накладываются на построенную +разметку командой python -m src.dxa.review_pack --merge; если в файле окажутся пути +из другого набора, команда сообщает об этом.

    + +
    Оценка модели в интерфейсе намеренно не показывается: разметчик, видя подсказку, +соглашался бы с ней, и поштучная разметка потеряла бы ценность независимого суждения. Инструмент не +заменяет эксперта — он фиксирует его суждение в формате, который пайплайн уже умеет читать.
    +

    12. Метрики качества

    12.1. Основная оценка

    -

    Оценка получена на фиксированном разбиении по исследованиям (обучение 199 снимков / 81 исследование, +

    Оценка получена на фиксированном разбиении по исследованиям (обучение 198 снимков / 81 исследование, валидация 53 снимка / 19 исследований, 16 нарушений), пять seed'ов обучения, эталон — вердикт эксперта из таблицы. Приоритетные по заданию метрики приведены с 95 % доверительными интервалами.

    КодСнимков
    rotation36
    rotation35
    artifact17
    axis_deviation10
    roi_incorrect7
    - - - - + + + +
    МетрикаЗначение95 % ДИКомментарий
    ROC-AUC0.6764[0.6309, 0.7218]приоритетная метрика задания
    PR-AUC0.4759[0.4141, 0.5377]базовый уровень при доле нарушений 30 % — около 0.30
    F10.5676[0.5270, 0.6082]порог подбирался по F1 на той же валидации, поэтому значение смещено вверх
    Recall / Precision0.700 / 0.486—рабочая точка выбранного порога
    ROC-AUC0.6726[0.6367, 0.7086]приоритетная метрика задания
    PR-AUC0.4753[0.4188, 0.5318]базовый уровень при доле нарушений 30 % — около 0.30
    F10.5559[0.5212, 0.5906]порог подбирался по F1 на той же валидации, поэтому значение смещено вверх
    Recall / Precision0.713 / 0.472—рабочая точка выбранного порога

    12.2. Оценка по областям и контрольные проверки

    - + - +
    Что измереноЗначениеКак получено
    ROC-AUC: позвоночник / бедро правое / бедро левое0.943 / 0.576 / 0.550рабочий чекпоинт, собственная валидация
    ROC-AUC: позвоночник / бедро правое / бедро левое0.929 / 0.606 / 0.567рабочий чекпоинт, собственная валидация
    Контрольная задача «позвоночник / бедро»AUC 1.00проверка работоспособности пайплайна, а не клиническая метрика
    Модель против правила «позвоночник = нарушение»0.854 против 0.529оценка на всём наборе, включая обучающие снимки, поэтому смещена вверх
    Модель против правила «позвоночник = нарушение»0.885 против 0.534оценка на всём наборе, включая обучающие снимки, поэтому смещена вверх

    Разбивка по областям нужна потому, что нарушения распределены неравномерно: в позвоночнике -33 из 99 снимков, у бёдер 21–22 из 73–79, а сама область почти однозначно определяется по ширине +33 из 99 снимков, у бёдер 20–22 из 73–78, а сама область почти однозначно определяется по ширине кадра. Поэтому общий AUC частично отражает различение области, а не только распознавание дефекта. Чтобы отделить одно от другого, выполнена проверка: правило «позвоночник = нарушение» даёт внутри -областей 0.50 (подсказки нет), модель — 0.85–0.93. Это означает, что модель использует содержимое +областей 0.50 (подсказки нет), модель — 0.88–0.95. Это означает, что модель использует содержимое снимка, а не только область.

    12.3. Выбор правила разметки

    - - - - + + + +
    Метрика (эталон)только таблицас суффиксами имён
    ROC-AUC0.6764 [0.6309, 0.7218]0.6199 [0.5840, 0.6559]
    PR-AUC0.4759 [0.4141, 0.5377]0.4046 [0.3702, 0.4391]
    F10.5676 [0.5270, 0.6082]0.5426 [0.5073, 0.5778]
    Recall / Precision0.700 / 0.4860.863 / 0.404
    ROC-AUC0.6726 [0.6367, 0.7086]0.6003 [0.5592, 0.6415]
    PR-AUC0.4753 [0.4188, 0.5318]0.3864 [0.3487, 0.4242]
    F10.5559 [0.5212, 0.5906]0.5354 [0.4978, 0.5729]
    Recall / Precision0.713 / 0.4720.788 / 0.409
    -

    Парная разница (только таблица минус с суффиксами): ROC-AUC +0.0564 [+0.0403, +0.0725], -PR-AUC +0.0713 [+0.0398, +0.1028] — знаки +++++, то есть преимущество на всех пяти +

    Парная разница (только таблица минус с суффиксами): ROC-AUC +0.0723 [+0.0541, +0.0905], +PR-AUC +0.0889 [+0.0500, +0.1277] — знаки +++++, то есть преимущество на всех пяти seed'ах. Принято правило «только экспертная таблица». Оговорка, важная для интерпретации: эталон — та же таблица, поэтому вариант, обучавшийся на ней, находится в выигрышном положении; значимо здесь то, что добавление ненадёжных пометок согласие с экспертом снижает, а не повышает.

    @@ -704,15 +749,15 @@ seed'ах. Принято правило «только экспертная т

    13.1. Измерения

    Измерения выполнены на рабочей машине разработчика (macOS, Apple Silicon) на полном наборе данных — -548 файлов в dataset_hack (544 обучающих плюс 4 тестовых). Инференс принудительно +482 DICOM-файла в dataset_hack (478 обучающих плюс 4 тестовых). Инференс принудительно переведён на CPU параметром --device cpu, чтобы числа не зависели от наличия ускорителя.

    - - - - + + + + @@ -769,10 +814,10 @@ seed'ах. Принято правило «только экспертная т

    Что проверено на самом образе. Сборка проходит три внутренние проверки: app import ok, frontend assets ok и -checkpoint ok: resnet18 linear threshold -0.4930129051208496. Контейнер запущен без единого +checkpoint ok: resnet18 linear threshold -0.13703127205371857. Контейнер запущен без единого монтирования (docker run -d -p 8000:8000 dxa-quality:cpu): чекпоинт присутствует внутри образа (/app/models/dxa_model.pth), /api/v1/health сообщает -model_loaded: true, device: cpu, эпоху 39 и файл разметки +model_loaded: true, device: cpu, эпоху 57 и файл разметки labels/labels_images.csv, запрос /api/v1/analyze отвечает корректной строкой результата, а /api/v1/export возвращает XLSX с ожидаемым набором столбцов. Сквозная проверка выполнена в контейнере на той же машине, где снимались измерения производительности. Оба @@ -809,8 +854,8 @@ CPU-образ содержит torch 2.8.0+cpu и ни одног

    15. Тесты и проверки

    -

    Тесты — pytest, 209 проверок в шести файлах; запуск — ./run.sh test или -python -m pytest tests/ -q. Сверка выполнена на дату документа: 209 passed.

    +

    Тесты — pytest, 272 проверки в девяти файлах; запуск — ./run.sh test или +python -m pytest tests/ -q. Сверка выполнена на дату документа: 272 passed.

    ПоказательЗначениеУсловия
    Время на изображение (медиана)0.021 сCPU, включает чтение DICOM, предобработку и проход сети
    Время на изображение (максимум)0.131 стот же прогон; первый снимок включает прогрев
    Пакет целиком (548 файлов)12.1 сот запуска процесса до записи XLSX, включая загрузку чекпоинта
    Доля успешно обработанных файлов548 / 548 (100 %)ошибок чтения на этом наборе нет
    Время на изображение (медиана)0.022 сCPU, включает чтение DICOM, предобработку и проход сети
    Время на изображение (максимум)0.041 стот же прогон
    Пакет целиком (482 файла)13.9 сот запуска процесса до записи XLSX, включая загрузку чекпоинта
    Доля успешно обработанных файлов482 / 482 (100 %)ошибок чтения на этом наборе нет
    Пиковая память процесса≈ 640 МБmaximum resident set size, пакетный CPU-инференс
    Ответ API на один файл16–18 мсвызов внутри процесса, после прогрева; первый запрос 390 мс. Здесь устройство выбрано автоматически (MPS), на CPU значение того же порядка — см. медиану пакетного прогона выше
    Время старта сервиса≈ 3 симпорт библиотек и загрузка чекпоинта
    @@ -818,11 +863,15 @@ CPU-образ содержит torch 2.8.0+cpu и ни одног - + + (table / union / expert), их согласованность, подключение + к обучению и чтение CSV, сохранённого Excel с BOM + + @@ -836,12 +885,20 @@ CPU-образ содержит torch 2.8.0+cpu и ни одног + + + +
    ФайлТестовЧто проверяет
    разбор имён, определение области и метки по имени, стратифицированное разбиение по исследованиям, отсутствие утечки, фиксация и переиспользование разбиения в файле, сверка с реальным датасетом
    tests/test_excel_labels.py50
    tests/test_excel_labels.py51 разметка по экспертной таблице: чтение критериев, правило «1 = нарушение», голосование по области, перенос оценки на единственное бедро, три правила метки - (table / union / expert), их согласованность и подключение - к обучению
    tests/test_manual_labels.py26ручная разметка: проверка вердикта (область, метка, совместимость типа нарушения с областью, + отказ от опечаток), хранение и правка вердиктов, чтение файла с BOM, наложение поверх построенной + разметки, подсчёт прогресса и выгрузка
    tests/test_rename_files.py36 приведение имён DICOM: разбор и канонизация, поиск свободного номера при конфликте, отказ угадывать область, цикл «применить → откатить»
    поля ответов, которые читает веб-интерфейс: базовый и детальный анализ, здоровье и карточка модели, выгрузка XLSX, обработка ошибок; различимость метрик между снимками и валидность PNG-визуализаций
    tests/test_labeling_api.py19контракт /api/v1/labeling/*: отказ отдавать файлы вне датасета (обход каталога, + не-DICOM), проверка вердикта, выгрузка, читаемая обучением как --labels-csv
    tests/test_review_pack.py17автономный пакет разметки: отпечаток набора, порядок «расхождения первыми», встроенные + данные разбираются как JSON, в странице нет ссылок на сеть, слияние вердиктов с построенной + разметкой и сообщение о путях из чужого пакета

    Отдельный контур — браузерные проверки интерфейса (tests/browser/*.js, Node и playwright-core): они открывают интерфейс во временном профиле Chrome, загружают DICOM, -кликают по строкам таблицы и снимают содержимое панели деталей. Требуется запущенный сервер на -127.0.0.1:8123; профиль пользователя не затрагивается.

    +кликают по строкам таблицы и снимают содержимое панели деталей. Для проверок интерфейса и ручной +разметки требуется запущенный сервер на 127.0.0.1:8123; проверке автономного пакета +сервер не нужен вовсе — она открывает файл прямо с диска. Профиль пользователя не затрагивается.

    @@ -851,8 +908,16 @@ CPU-образ содержит torch 2.8.0+cpu и ни одног словарём + + + +
    СценарийЧто проверяет
    ui_violation.js ветку «нарушение»: бейдж, POOR, HIGH, текст заключения
    ui_labeling.jsинтерфейс ручной разметки: список снимков с прогрессом, расхождения первыми, отрисовку + снимка, сохранение вердикта и его живучесть после перезагрузки страницы, фильтр «только + расхождения», отсутствие внешних запросов
    ui_offline.js отсутствие обращений страницы к внешним хостам
    review_pack.jsавтономный пакет разметки — без сервера вообще: файл открывается с диска, встроенный снимок + рисуется, вердикт сохраняется и переживает перезагрузку, CSV выгружается и загружается обратно, + ни одного сетевого запроса

    16. Известные ошибки и их обработка

    @@ -915,9 +980,10 @@ CPU-образ содержит torch 2.8.0+cpu и ни одног
  • Разметка выведена из оценки исследования, а не снимка. Экспертная таблица описывает исследование; перенос вердикта на снимок однозначен (область встречается один раз), но поштучной экспертной оценки снимков в наборе нет. Оценка качества модели упирается в качество - этой разметки, а не только в объём данных.
  • -
  • Мало данных. 252 уникальных снимка, 77 нарушений. Доверительные интервалы широкие - (ROC-AUC 0.6764 [0.6309, 0.7218] по пяти seed'ам), поэтому оценка на закрытом наборе может + этой разметки, а не только в объём данных. Для поштучной разметки в сервисе есть интерфейс + /label (раздел 11.1), им ещё не пользовались.
  • +
  • Мало данных. 251 уникальный снимок, 76 нарушений. Доверительные интервалы широкие + (ROC-AUC 0.6726 [0.6367, 0.7086] по пяти seed'ам), поэтому оценка на закрытом наборе может отличаться.
  • Эталон — та же таблица. Независимой истины нет, поэтому сравнение правил разметки частично благоприятствует варианту «только таблица» (раздел 12.3).
  • @@ -938,16 +1004,17 @@ CPU-образ содержит torch 2.8.0+cpu и ни одног
  • Числовые эвристики панели деталей не калиброваны. Их пороги рассчитаны на другой масштаб интенсивностей, поэтому в интерфейсе показываются измерения, а не вердикты.
  • Рабочая точка порога даёт высокий recall при умеренной точности. На обучающем наборе при - пороге, подобранном по F1, модель относит к нарушениям 341 строку из 548 (≈ 62 %) при - фактической доле нарушений 30.6 %, что согласуется с precision 0.486. Порог выбран в пользу + пороге, подобранном по F1, модель относит к нарушениям 204 строки из 482 (≈ 42 %) при + фактической доле нарушений 30.3 %, что согласуется с precision 0.472. Порог выбран в пользу полноты: пропустить непригодное исследование дороже, чем показать лишнее.
  • 18. План развития