NEUROPEOPLEDensitoAI Назад на главную Открыть кабинет

Под капотом

Как устроен сервис, полные таблицы метрик, стенд устойчивости к искажениям и команды, которыми всё воспроизводится.

Задача и рамки

Что оценивает сервис

Вход — DICOM-файлы денситометрии GE Lunar Prodigy (прямые проекции поясничного отдела позвоночника и проксимального отдела бедра). Выход — CSV из девяти колонок в заданном порядке: область, признак нарушения, тип нарушения из закрытого перечня, вероятность и время обработки. Проверяются пять критериев ТЗ: укладка и ось позвоночника, посторонние предметы, поворот бедра и область интереса бедра. Медицинского заключения сервис не формирует.

Как работает

Как устроено решение

Один контур измеряет геометрию, второй смотрит на снимок целиком. Решение по критерию — их согласование, порог зафиксирован на out-of-fold.

DICOM на входе

Папка, zip или файл. Сбой одного снимка не останавливает партию.

Область

Позвоночник или бедро — по размеру кадра и тегам, для нестандартных — по содержимому.

Два контура

A — измерения, B — эмбеддинги EfficientNet-B0. Обучены отдельно на каждый критерий.

Ранговый стэкинг

Скор — среднее рангов двух контуров. Порог взят на OOF, не на обучении.

Отчёт

CSV по ТЗ, XLSX по исследованиям, технический CSV, PNG и карточка решения.

Как устроены оба контура и откуда берётся порог — признаки, модели, стэкинг

Контур A — геометрия

Сегментация кости, измерение наклона оси, смещения от центра кадра, ширины костной области, угла диафиза бедра, расстояния до бокового края, площади плотных включений. Логистическая регрессия на 1–5 признаках.

axis_angle_deg, center_offset_ratio, shaft_angle_deg, edge_distance_ratio, metal_area_mm2
+

Контур B — эмбеддинги

EfficientNet-B0 даёт вектор 1280 признаков, затем стандартизация, PCA до 32 компонент и логистическая регрессия с сильной регуляризацией. Для укладки позвоночника используется дообученный на наших данных бэкбон.

1280-d → StandardScaler → PCA32 → LogReg C=0.1
=

Стэкинг и порог

score = 0.5·rank(геометрия) + 0.5·rank(эмбеддинги). Пороги для каждого критерия зафиксированы по out-of-fold предсказаниям с группировкой по исследованию; для редкого критерия укладки позвоночника порог взят по доле позитивов, чтобы не подстраиваться под шум.

config.yaml · models/metrics_summary.json
Метрики

Метрики out-of-fold

Все оценки — out-of-fold с группировкой по исследованию: модель никогда не видела снимки того исследования, на котором проверяется. Рядом с каждым критерием — положение AUC стека на шкале 0.5–1.0 и интервал F1 по исследованиям. Слабые места показываем, а не скрываем.

Полоса — положение ROC-AUC стека (OOF) на шкале 0.5–1.0; левый край — случайный классификаторинтервалы F1 — бутстрап по исследованиям, 95 %
КритерийОбластьn / позит.AUC геом.AUC эмб.AUC стекAUC nestedAUC на шкале 0.5–1.0F1 OOF [95 % ДИ]Надёжность
Укладка пациента
sp_pos · порог по доле позитивов, порог 0.861
позвоночник166 / 100.9150.7900.8930.8780.50 [0.12; 1.00]мало позитивов (10): интервал широкий, читать по AUC; сильнее контур A (геометрия + признак головы укладки)
Ось позвоночника
sp_axis · prevalence_x1.4, порог 0.777
позвоночник166 / 170.8390.7970.8890.8600.60 [0.29; 0.82]сильнее геометрия
Посторонние предметы
sp_art · prevalence_x1.4, порог 0.624
позвоночник166 / 350.8040.8970.8990.8840.67 [0.39; 0.83]сильнее контур B (эмбеддинги)
Укладка пациента
hip_pos · порог по доле позитивов, порог 0.658
бедро, обе стороны329 / 790.7100.6350.7250.7090.43 [0.22; 0.60]сильнее геометрия
Область интереса
hip_roi · порог по доле позитивов, порог 0.918
бедро, обе стороны329 / 160.9020.8770.9170.8730.51 [0.00; 0.91]сильнее геометрия

ROC-AUC стека по пяти критериям — от 0.725 до 0.917. У критериев с малым числом положительных исследований (sp_pos — 6, hip_roi — 6) интервалы широки: оценка описывает несколько конкретных исследований, а не устойчивую способность распознавать дефект (паспорт выборки — docs/sample_passport.json). AUC nested — оценка поставленной модели без подгонки порога и веса стэкинга (вложенная проверка по группам «исследование + хэш пикселей», поле nested_auc_production); она ниже OOF на 0.015–0.044. F1 hip_roi 0.514 зависит от одного исследования: у 5 кадров в 1 исследовании метки правого и левого бедра различаются, и при старой плотностной привязке стороны F1 0.267 (docs/metrics_side_label.json). Слабые места показываем, а не скрываем. Источник: models/metrics_summary.json (OOF, группировка по исследованию), ДИ по F1 — бутстрап по исследованиям.

Бинарная задача «есть нарушение» по областям, OOF

ОбластьROC-AUC [95 % ДИ]F1 [95 % ДИ]Macro-F1 по типам нарушений [95 % ДИ]
Поясничный отдел позвоночника0.846 [0.73; 0.94]0.737 [0.57; 0.85]0.590 [0.44; 0.74]
Проксимальный отдел бедра0.760 [0.63; 0.86]0.516 [0.31; 0.68]0.473 [0.20; 0.68]

Источник: docs/METRICS_REPORT.md, таблица «Итого» (python src/eval_oof_metrics.py, 95 % ДИ — бутстрап по исследованиям, 2000 повторов).

Методика валидации: общий пул исследований и три трактовки macro-F1, OOF

ПоказательПо файлам [95 % ДИ]По уникальным кадрам [95 % ДИ]
Бинарная ROC-AUC по quality_prob, все файлы одним пулом0.796 [0.70; 0.88]0.794 [0.72; 0.87]
Бинарная F1 по quality_class, все файлы одним пулом0.608 [0.49; 0.71]0.586 [0.48; 0.68]
Macro-F1 по областям: позвоночник / бедро0.590 / 0.4730.645 / 0.491
Macro-F1 по 5 критериям0.544 [0.40; 0.67]0.583 [0.43; 0.68]
Macro-F1 по 4 строкам словаря («Некорректная укладка» общая)0.557 [0.40; 0.67]0.549 [0.38; 0.65]
Уровень визита: чувствительность0.792 [0.67; 0.90]0.792 [0.67; 0.90]
Уровень визита: специфичность0.615 [0.48; 0.75]0.615 [0.48; 0.75]

Источник: docs/ORGANIZER_METRICS.md (python tools/organizer_metrics.py --oof; разметка по исследованиям, лист «Калибровка»; кластерный бутстрап по исследованиям). Файлов 495, уникальных кадров 249, визитов 100 (с нарушением 48). У sp_pos 18 флагов при 10 позитивах: 10 строк — клоны одного кадра без нарушения ровно на пороге.

Бейзлайны на тех же OOF-строках: что даёт архитектура

Те же кадры и разбиение, что у метрик выше. ROC-AUC «всегда норма», «всегда нарушение» и случайного классификатора равен 0.5; F1 «всегда нарушение» — нижняя планка F1 при доле позитивов p, равная 2p/(1+p). Контуры A и B и простая логрегрессия на всех геометрических признаках без отбора (GroupKFold(5) по исследованию) оценены при том же правиле порога на собственных OOF-скорах. Интервалы ROC-AUC — бутстрап по исследованиям, 500 ресэмплов.

Критерийn / позит.F1 «всегда нарушение»AUC контур AAUC контур BAUC все признаки без отбораAUC стек [95 % ДИ]F1 стек
Укладка пациента
sp_pos · позвоночник
166 / 100.110.9150.7900.8370.893 [0.70; 1.00]0.50
Ось позвоночника
sp_axis · позвоночник
166 / 170.190.8390.7970.5100.889 [0.75; 0.98]0.60
Посторонние предметы
sp_art · позвоночник
166 / 350.350.8040.8970.4820.899 [0.81; 0.97]0.67
Укладка пациента
hip_pos · бедро
329 / 790.390.7100.6350.5120.725 [0.62; 0.82]0.43
Область интереса
hip_roi · бедро
329 / 160.090.9020.8770.3730.917 [0.71; 1.00]0.51

Стек не везде выше сильнейшего контура по AUC (sp_pos): его вес 0.5/0.5 зафиксирован заранее ради устойчивости, а не подобран под эту выборку. У sp_pos контур A при пороге по доле позитивов помечает 10 клонов одного отрицательного исследования и даёт F1 = 0 при AUC 0.915. Логрегрессия на всех признаках без отбора теряет сигнал сильных одиночных признаков при 10–35 позитивах. Таблица описывает различия внутри одной выборки и не доказывает перенос на другие аппараты и разметчиков. Источник: docs/BASELINES.md, docs/baselines.json.

Как читать quality_prob

quality_prob — грубая вероятность, не точная: на OOF ошибка калибровки ECE 0.14 (позвоночник) и 0.15 (бедро), Brier 0.166 / 0.198 против 0.231 / 0.201 у константы. Значение выше 0.5 означает «сервис поставил класс 1»: при 0.5–0.8 нарушение подтверждается у 44 % кадров позвоночника, при 0.8 и выше — у 75 %; для бедра при 0.5–0.9 — у 36 %, при 0.9 и выше — у 96 %. Внутри класса это ранг для сортировки очереди, а не процент. Скоры критериев — ранги по построению; калиброванная величина p_cal (debug-CSV, API) читается как вероятность только для посторонних предметов и укладки бедра, где положительных исследований 17 и 24. Разбор с диаграммами надёжности и интервалами — docs/CALIBRATION.md. Зона «не уверен» (needs_review) сейчас помечает 13.9 % строк позвоночника и 9.4 % строк бедра; в неё попадают в основном строки с уже отмеченным нарушением, пропусков в ней мало (3 из 11 и 6 из 44 на OOF). Расширение полосы вокруг порогов не поднимает полноту среди уверенных строк за пределами шума, а лишь переводит строки в зону почти линейно; разбор по ширине полосы — docs/UNCERTAIN_ZONE.md.

Честные ограничения

Ограничения применимости

Шесть исследований с нарушением укладки позвоночника

В обучающей выборке нарушение укладки позвоночника встретилось в 6 исследованиях из 99 (10 кадров из 166; половину дают 2 исследования). Метка задана на исследование, поэтому единица счёта — исследование: 166 кадров позвоночника несут информацию 99 исследований, 329 кадров бедра — 78. Интервалы считаются по исследованиям и для редких критериев широки: F1 укладки позвоночника 0.50 [0.12; 1.00], области интереса бедра 0.51 [0.00; 0.91]. Признак головы укладки (2.4.0) проверен nested только на этих же 6 исследованиях, поэтому на закрытом тесте по укладке позвоночника следует ожидать результат заметно слабее OOF-цифр. Источник: docs/sample_passport.json.

Один аппарат, одна клиника, один разметчик

Все 499 снимков получены на одном GE Lunar Prodigy и размечены одним экспертом. Перенос на другие денситометры не проверен. Часть меток опирается на признаки, которые наши измерения ловят частично: например, у эксперта ось позвоночника оценивается шире, чем глобальный наклон.

Посторонние предметы: признак зоны измерения — решение владельца проекта

С 2.5.0 контур A считает плотные участки только в зоне измерения — верхних 70 % протяжённости кости; прежний признак в основном видел крылья подвздошных костей внизу кадра. ROC-AUC стека sp_art на OOF 0.823 → 0.899, F1 0.535 → 0.667 [0.39; 0.83]. Правило проекта (прирост ≥ 0.03 в ≥ 70 % повторов вложенной проверки) пройдено — 19 из 20, калиброванный парный гейт — нет (p 0.072), поэтому признак включён решением владельца проекта, как признак головы укладки в 2.4.0. На 40 кадрах слепой проверки врачами эффекта нет: совпадение с разметкой 27 → 28 из 40. На синтетических фантомах позвоночника флаг предметов стоит на всех трёх кадрах, где предмета нет (норма, наклон оси, смещение), — эталон verify обновлён, в ограничениях это указано. На контрольном образце «Для теста» снимок CR000000_ПОП.dcm получает «Присутствуют посторонние предметы» (0.952; в 2.4.x — норма 0.325, у порога): в зону попадают концы рёбер в верхних углах кадра, 303 мм². Ограничение зоны по ширине (5–40 мм от позвоночника) проверено вложенной проверкой 28.09 и не принято: у 10 из 32 кадров с предметом по разметке все плотные участки дальше 40 мм от позвоночника. Источник: docs/NESTED_GATE_REPORT.md, часть 8.

Широкое поле бедра

Ложные тревоги по укладке бедра чаще на кадрах с широким полем. В 2.5.0 флаг и класс остаются, но при боковом запасе поля больше 51 мм (верхняя терциль по 333 снимкам бедра, задана до расчёта) команда лаборанту — «Проверить укладку по малому вертелу», а не «Переснять»: на OOF так меняется команда у 27 из 134 снимков с «Переснять», все — бедро; среди флагов укладки бедра с широким полем верных 11 из 31, и у них теперь «Проверить». Окно фиксированного размера в миллиметрах проверено и не внедрено: прирост AUC +0.027, порог правила пройден в 11 из 20 повторов при нужных 14, парный гейт p 0.19.

Подсказки 2.5.0 — механизм, эффект не заявлен

Второе мнение по оси (контуры разошлись больше чем на 0.45) ловит 55 % ошибок оси во вложенной проверке, но нагружает 24 % снимков позвоночника (в худшем повторе 25.3 %), сигнал слабый — AUC 0.56. Роли критериев («измерение» и «подсказка, решает врач») и второй тур экспертной проверки «врач + сервис» — механизм: у врача 1 пара «врач + измерение» дала индекс Юдена 0.424 → 0.584, на 40 кадрах это не значимо (p 0.375), ответов второго тура от врачей пока нет. Сводка врачей относится к версии 2.4.0.

Устойчивость к искажениям

Устойчивость к искажениям

Измеренные заранее кривые, а не живые ползунки: каждый вид искажения прогнан на выборке размеченных снимков, показана доля снимков, у которых изменился класс качества.

пересохранение файла 0.0 % (0/81) удалены теги (шаг пикселя, описание) 0.0 % (0/81) нет UID исследования и снимка 0.0 % (0/81) инвертированная шкала (MONOCHROME1) 0.0 % (0/81) 16-битный контейнер 0.0 % (0/81) обрезка 4 % по краям 16.0 % (13/81) уменьшение ×0,8 с пересчётом шага 19.8 % (16/81) увеличение ×1,25 с пересчётом шага 8.6 % (7/81) шум 3 % 27.2 % (22/81) ярче (гамма 0,7) 18.5 % (15/81) темнее (гамма 1,4) 17.3 % (14/81) 0 % 10 % 20 % 30 %
формат файла и тегипиксели: яркость, шум, масштаб, обрезка

Выборка: 81 снимков разметки (27 позвоночник, 54 бедро), из них с нарушениями 40. Каждый снимок пересохранён с одним искажением и прогнан повторно; показана доля снимков, у которых изменился класс качества. Ошибок обработки (Failure) ни в одном варианте не было (максимум 0.0 %), область снимка не менялась ни разу (максимум 0.0 %). Время на снимок в исходном прогоне: медиана 0.05 с, 95-й перцентиль 0.09 с (стенд 2 vCPU; паспорт производительности с условиями — docs/PERFORMANCE.md).

На реальном потоке одного аппарата GE Lunar с фиксированным экспортом искажений яркости, шума и масштаба нет: все снимки приходят в одном формате. Изменения формата и тегов на потоке возможны, и к ним система устойчива полностью. Перевороты при изменении экспозиции сосредоточены у порога решения (медианный запас 0,09–0,11), то есть касаются спорных снимков.

Источник: docs/qa/robustness_results.json (2026-09-20), docs/ROBUSTNESS_REPORT.md; скрипт tests/robustness_suite.py.

Как проверить

Как проверить результат

Техническая группа может развернуть сервис на своих данных без нашего участия. Сервис не делает ни одного внешнего вызова: кабинет, API и модели лежат внутри образа, и самопроверка из 18 пунктов прогоняется в контейнере с полностью отключённой сетью (--network none).

Команды запуска, список документов и состав контейнера — для тех, кто будет разворачивать

Docker: сборка, пакетный прогон, API

# сборка образа и самопроверка
./build_and_run.sh build
# пакетная обработка папки с DICOM или zip -> outputs/results.csv (+ .xlsx)
./build_and_run.sh run /path/to/dicom_folder ./outputs
# HTTP API на порту 8000, Swagger: http://localhost:8000/docs
./build_and_run.sh api 8000 /path/to/dicom_folder ./outputs
# тесты формата и изоляции запросов
./build_and_run.sh test

Приём по DICOM

Снимки можно не загружать вручную: контейнер принимает их с денситометра или из PACS по DICOM C-STORE (AE Title DENSITOAI, порт 11112; CR, DX, Secondary Capture; несжатые transfer syntax и RLE). Принятое исследование уходит в тот же /api/analyze, что и кабинет, результат появляется в outputs/jobs/<job_id>/, принятые файлы остаются в outputs/inbox/. Запуск: docker compose up -d densito-api densito-receiver; проверка: python -m pynetdicom storescu <ip> 11112 tests/phantoms/study_01 -r -aec DENSITOAI. Без TLS — только внутренняя сеть отделения. Подробнее — docs/DICOM_RECEIVER.md.

Без Docker

pip install -r requirements.txt
python src/inference.py --input /path/to/dicom --output outputs/results.csv --xlsx
python tests/test_inference_format.py      # формат, битые входы, детерминизм
python tests/test_api_isolation.py         # изоляция файлов между запросами
python src/eval_oof_metrics.py             # метрики с ДИ -> docs/metrics_oof_full.md

Код и документы

  • git clone https://github.com/neuropeopleteam-afk/densitoai.gitпубличный репозиторий
  • /docsSwagger UI работающего API: открыть; с 2.4.1 работает без интернета — файлы swagger-ui лежат в образе (web/assets/swagger/), эта страница и главная тоже открываются в локальном контейнере
  • /api/results/{job_id}/additional_series.zip(2.4.1) zip-архив дополнительных серий по ТЗ п. 2.7: DICOM SR на исследование, наложение SC, SEG; нужен код доступа запроса, ссылка — поле additional_series_zip_url ответа /api/analyze; в пакетном режиме архив пишется рядом с results.csv, в кабинете — кнопка «Скачать дополнительные серии (zip)»
  • README.mdназначение, запуск, API, формат данных, ограничения
  • tools/department_summary.pyсводка по партии для отделения: доли нарушений по области, типу, аппарату и дате с ДИ Уилсона 95 %, без персональных данных; в кабинете — блок «Сводка по партии», в API — /api/results/{job}/summary
  • docs/VERIFICATION.md самопроверка образа из 18 пунктов, в том числе стресс-набор: 24 битых и нестандартных файла из фантомов в одном пакете с нормой — битые дают строку Failure, норма совпадает с одиночным прогоном бит в бит
  • docs/MEASUREMENT_CHECK.mdакт поверки измерительного контура на фантомах: 13 фантомов повёрнуты на −15…+15° и сдвинуты на −30…+30 мм, измеренное сравнено с заданным без разметки — в диапазоне до 8° и 20 мм угол оси позвоночника MAE 0.35°, угол диафиза бедра MAE 0.04°, смещение центра MAE 0.09 мм, сторона бедра 245/245; поверка на фантомах, не замена клинической валидации
  • docs/METRICS_REPORT.mdметрики по ТЗ §8.4 с 95 % ДИ
  • docs/PERFORMANCE.mdпаспорт производительности: секунды на снимок и память с условиями замера; боевой сервер в тихих условиях — медиана 0,54 с (499 файлов за 368 с одним процессом), под чужой нагрузкой — 1,84 с (прогон версии 2.3.2), локально 2 vCPU — 0,07 с; как воспроизвести, что числа не означают
  • docs/UNCERTAIN_ZONE.mdзона «не уверен»: кривая «покрытие → полнота» по ширине полосы на OOF, ДИ по исследованиям, проекция на боевой прогон; правило не меняется
  • docs/ROBUSTNESS_REPORT.mdустойчивость к нестандартному входу
  • docs/PREPROC_GATE_REPORT.mdинвариантная предобработка и выбор варианта внутри nested
  • docs/EVIDENCE.mdданные, лицензии, отвергнутые гипотезы, ограничения
  • models/MODEL_CONTRACT.mdформат моделей и порогов
  • config.yamlвсе строки, пороги и веса в одном файле

Что внутри контейнера

Python 3.12, PyTorch CPU, scikit-learn, pydicom. Модели — файлы pkl с зафиксированным контрактом, бэкбон EfficientNet-B0 и этот самый кабинет лежат в образе — интерфейс открывается на корне того же порта, что и API. Ошибки отдельных файлов не роняют запрос: строка получает статус Failure и вероятность 0.5.

Пример вывода

Пример вывода

Визуализация сервиса: снимок поясничного отдела с найденной осью, контуром кости и плотными посторонними объектами
Решение: проверить и при подтверждении переснять
Реальный вывод сервиса на снимке из выборки заказчика. Оранжевая линия — измеренная ось (наклон 3.1° к вертикали), зелёный контур — костная область, красные рамки — плотные объекты (188 мм², контраст к кости 4.2 сигмы). Порог превысил один критерий — посторонние предметы 0.955 при 0.624 (из 188 мм² плотных участков 135 мм² — в зоне измерения); ось 0.651 при 0.777 и укладка 0.223 при 0.861 — в норме (версия 2.5.0). Эксперт на этом снимке отметил ровно посторонние предметы: совпадение полное. Версия 2.2.0 на этом же снимке дополнительно поднимала флаг по оси (0.864 при 0.756) — ложное срабатывание, которое убрал новый бэкбон контура B (К13).

Выборка и режим работы

  • 499 снимков из 100 исследований в обучающей выборке (252 уникальных кадра, остальное — клоны экспорта), разметка одного эксперта; обе области — в 78 исследованиях.
  • Две области: поясничный отдел позвоночника и проксимальный отдел бедра.
  • Пять критериев: укладка, ось, посторонние предметы, укладка бедра, область интереса.
  • Время на снимок зависит от машины и нагрузки: на боевом сервере в тихих условиях медиана 0,54 с, p95 1,76 с (499 снимков за 368 с одним процессом), во время чужой нагрузки медиана 1,84 с (p95 6,11 с; прогон версии 2.3.2), локально на 2 vCPU 0,07 с; с визуализацией через API 1–2 с. Условия и способ воспроизведения — docs/PERFORMANCE.md; пропускная способность не обещается.
  • При нескольких исследованиях в задаче кабинет показывает «Сводку по партии»: доли снимков и исследований с нарушением по области, типу и аппарату с доверительными интервалами Уилсона 95 %, зону «не уверен», причины Failure и UID для пересмотра. На обучающей выборке: файлов с нарушением 41,7 % [37,4; 46,1], исследований с нарушением 72,0 % [62,5; 79,9]. Персональных данных в сводке нет.