develop - hack_2026

This commit is contained in:
denis 2026-09-23 00:37:02 +03:00
parent 3121ecb8a3
commit b67245270d
13 changed files with 1513 additions and 108 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,652 @@
# Аналитический документ: DXA Quality Assessment System
## Содержание
1. [Обзор системы](#обзор-системы)
2. [Архитектура](#архитектура)
3. [Компоненты системы](#компоненты-системы)
4. [Пайплайн обработки](#пайплайн-обработки)
5. [API Endpoints](#api-endpoints)
6. [Модели и алгоритмы](#модели-и-алгоритмы)
7. [PlantUML Диаграммы](#plantuml-диаграммы)
---
## Обзор системы
Система DXA Quality Assessment — это медицинский AI-сервис для автоматизированной оценки качества исследований DXA (денситометрия костей). Система анализирует DICOM файлы и определяет:
- **Анатомическую область**: позвоночник (spine) или бедро (hip_left/hip_right)
- **Качество изображения**: OK (класс 0) или нарушение (класс 1)
- **Тип нарушения**: движение, артефакты, позиционирование, ROI, ротация
### Целевое назначение
| Параметр | Значение |
|----------|----------|
| Тип | Медицинский AI сервис |
| Входные данные | DICOM файлы (DXA исследования) |
| Выходные данные | JSON / XLSX отчеты |
| Тип классификации | Бинарный (OK / Violation) |
| Целевые регионы | Позвоночник (L1-L4), Бедро (левое/правое) |
---
## Архитектура
### Высокоуровневая архитектура
```
┌─────────────────────────────────────────────────────────────────────────┐
│ DXA Quality Assessment │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ Клиент │───▶│ FastAPI │───▶│ Orchestrator │ │
│ │ (Web UI) │ │ Server │ │ (Pipeline Control) │ │
│ └──────────────┘ └──────────────┘ └───────────┬──────────────┘ │
│ │ │
│ ┌───────────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌───────────────┐ ┌─────────────┐ ┌──────────┐ │
│ │ DXA Classifier │ │ Segmentor │ │ Quality │ │
│ │ (ResNet18) │ │ (Threshold) │ │ Scorer │ │
│ └───────────────┘ └─────────────┘ └──────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ Detailed Assessment Module │ │
│ │ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌───────┐ ┌────────┐ │ │
│ │ │ Motion │ │ Artifact │ │ Position│ │ ROI │ │Rotation│ │ │
│ │ │Detector │ │ Detector │ │Validator│ │ Check │ │ Checker│ │ │
│ │ └─────────┘ └──────────┘ └─────────┘ └───────┘ └────────┘ │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### Технологический стек
| Компонент | Технология |
|-----------|------------|
| Backend | Python 3.10, FastAPI, Uvicorn |
| ML/DL | PyTorch, torchvision (ResNet18) |
| Image Processing | PIL, OpenCV, pydicom, scipy |
| Data Handling | pandas, openpyxl |
| Containerization | Docker |
---
## Компоненты системы
### 1. API Layer (`src/main.py`)
Точка входа — FastAPI приложение с REST эндпоинтами.
**Основные функции:**
- Прием DICOM файлов через multipart/form-data
- Предобработка изображений (нормализация, ресайз до 224x224)
- Инференс модели
- Формирование ответов
```python
# Основной API эндпоинт
@app.post("/api/v1/analyze/detailed")
async def analyze_dicom_detailed(file: UploadFile = File(...)):
# 1. Загрузка модели
# 2. Предобработка DICOM
# 3. Инференс ResNet18
# 4. Генерация сегментации
# 5. Детальная оценка качества
# 6. Формирование ответа
```
### 2. DXA Classifier (`src/dxa/model.py`)
Модель для бинарной классификации качества изображения.
**Архитектура:**
```
Input (224x224x3)
│
▼
ResNet18 (pretrained on ImageNet)
│
├── Remove final FC layer
│
▼
Classifier Head:
├── Dropout(0.3)
├── Linear(512 → 256)
├── ReLU
├── Dropout(0.3)
└── Linear(256 → 2)
│
▼
Output: [prob_OK, prob_Violation]
```
**Параметры модели:**
- Backbone: ResNet18 (ImageNet pretrained)
- Input: 224x224 RGB
- Output: 2 класса (OK / Violation)
- Dropout: 0.3
### 3. Segmentator (Простая реализация)
Для сегментации используется простой пороговый метод:
```python
# Threshold-based segmentation
threshold = np.percentile(image, 90)
segmentation = (image > threshold).astype(np.uint8)
```
**Планируется:** Замена на UNet или TotalSegmentator для более точной сегментации.
### 4. Detailed Assessment (`src/quality/detailed_assessment.py`)
Модуль детальной оценки качества включает:
#### 4.1 Motion Detection
- **Laplacian variance** — дисперсия лапласиана (ниже = размытие)
- **FFT blur** — анализ высокочастотной энергии (ниже = размытие)
- **Edge duplication** — проверка "призрачных" краев
#### 4.2 Artifact Detection
- **Metal detection** — яркие пятна (>99.5 перцентиль)
- **Implant detection** — линейные структуры
- **Cement detection** — локальные яркие области
- **Calcification** — малые яркие пятна
#### 4.3 Region-Specific Checks
**Для позвоночника (spine):**
- Проверка количества позвонков (3-4 для поясничного отдела)
- Проверка полноты (не обрезаны ли позвонки)
- Проверка выравнивания позвонков
- Проверка контуров позвонков
**Для бедра (hip):**
- Проверка полноты видимости (шейка бедра, головка)
- Проверка ротации (угол главной оси)
- Проверка соотношения сторон
#### 4.4 ROI Validation
- Проверка отступов от краев
- Проверка размера ROI
### 5. Region Detector (`src/dxa/inference.py`)
Определение анатомической области по содержимому изображения:
**Алгоритм:**
1. Пороговая бинаризация (95 перцентиль)
2. Вычисление bounding box яркой области
3. Соотношение сторон bbox:
- < 1.5 → **spine** (более квадратная область)
- ≥ 1.5 → **hip** (вытянутая область)
4. Для hip: определение левого/правого по асимметрии яркости
---
## Пайплайн обработки
### Основной пайплайн
```plantuml
@startuml
title DXA Quality Assessment Pipeline
start
:Upload DICOM file;
:Parse DICOM metadata;
:Preprocess image;
note right
- Normalize to 0-1
- Convert to 3-channel
- Resize to 224x224
end note
:Model Inference (ResNet18);
note right
- Get prediction (OK/Violation)
- Get confidence scores
end note
:Determine Anatomical Region;
note right
- bbox_aspect ratio analysis
- left/right brightness ratio
end note
:Generate Segmentation;
note right
- Threshold-based (90th percentile)
end note
:Detailed Quality Assessment;
note right
- Motion detection
- Artifact detection
- Region-specific checks
- ROI validation
end note
:Determine Violation Type;
note right
- correct
- position_error
- artifact_motion
- artifact_other
- labeling_error
- incomplete_view
- roi_error
- rotation
end note
:Generate Response;
stop
@enduml
```
### Детальная диаграмма последовательности
```plantuml
@startuml
title Sequence: Detailed Analysis
actor User
participant "FastAPI" as API
participant "DXA Model" as Model
participant "Region Detector" as Region
participant "Segmentator" as Seg
participant "Quality Assessor" as QA
User -> API: POST /api/v1/analyze/detailed
API -> API: preprocess_dicom()
API -> Model: predict(image)
Model --> API: [prediction, confidence]
API -> Region: determine_region(image)
Region --> API: "spine" | "hip_left" | "hip_right"
API -> Seg: segment(image)
Seg --> API: segmentation_mask
API -> QA: generate_quality_report()
note over QA
- detect_motion_blur()
- detect_artifacts()
- check_spine_completeness() / check_hip_completeness()
- check_hip_rotation()
- check_roi_boundaries()
- determine_violation_type()
end note
QA --> API: quality_report
API -> API: convert_to_serializable()
API --> User: JSON response
@enduml
```
---
## API Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/` | Web interface |
| GET | `/api/v1/health` | Health check |
| POST | `/api/v1/analyze` | Basic analysis |
| POST | `/api/v1/analyze/detailed` | Detailed analysis with metrics |
| POST | `/api/v1/analyze/sr` | DICOM SR report |
| POST | `/api/v1/batch` | Batch processing |
| POST | `/api/v1/export` | Export to XLSX |
### Response Format (Detailed)
```json
{
"anatomical_region": "spine",
"quality_class": 1,
"quality_label": "Violation detected",
"violation_type": "artifact_motion",
"reason": "Обнаружен артефакт движения (размытие)",
"confidence": 0.85,
"confidence_per_class": {
"correct": 0.15,
"violation": 0.85
},
"view_quality": "full",
"metrics": {
"motion": {
"motion_detected": true,
"blur_laplacian": 0.001,
"severity": "HIGH"
},
"artifacts": {
"any_detected": false,
"metal_detected": false
},
"roi_check": {
"valid": true
}
},
"overall_quality": "POOR",
"severity": "HIGH"
}
```
---
## Модели и алгоритмы
### Типы нарушений
**Для позвоночника:**
- `correct` — качество соответствует норме
- `position_error` — ошибка позиционирования
- `artifact_motion` — артефакт движения (размытие)
- `artifact_other` — другие артефакты
- `labeling_error` — ошибка маркировки
- `incomplete_view` — неполный вид
- `roi_error` — ошибка ROI
**Для бедра:**
- `correct` — качество соответствует норме
- `position_error` — ошибка позиционирования
- `rotation` — нарушение ротации
- `artifact_motion` — артефакт движения
- `artifact_other` — другие артефакты
- `roi_error` — ошибка ROI
- `incomplete_view` — неполный вид
### Метрики детекции движения
| Метрика | Порог | Описание |
|---------|-------|----------|
| Laplacian variance | < 0.002 | Низкая дисперсия = размытие |
| FFT high-freq ratio | < 0.3 | Низкая высокочастотная энергия |
| Edge duplication | bool | Дублирование краев |
### Алгоритм определения региона
```plantuml
@startuml
title Region Detection Algorithm
start
:Load DICOM image;
:Normalize to 0-1;
:Threshold at 95th percentile;
if (binary.sum > 0?) then (yes)
:Calculate bounding box;
:Compute bbox_aspect = height / width;
if (bbox_aspect < 1.5) then (yes)
:return "spine";
else (no)
if (bbox_aspect < 1.8) then (yes)
:Calculate symmetry;
if (symmetry > 0.35) then (yes)
:return "spine";
else (no)
:return "hip";
end
else (no)
:Calculate left/right brightness ratio;
if (ratio > 1.3) then (yes)
:return "hip_right";
else if (ratio < 0.7) then (yes)
:return "hip_left";
else (no)
:return "hip";
end
end
end
else (no)
:Fallback: height < 270 → hip;
:return "spine";
end
stop
@enduml
```
---
## PlantUML Диаграммы
### Диаграмма компонентов
```plantuml
@startuml
!theme plain
skinparam componentStyle uml2
component "Web Client" as Client {
[Web UI]
}
component "FastAPI Server" as API {
[Upload Handler]
[Preprocessor]
[Response Builder]
}
component "DXA Models" as Models {
[ResNet18 Classifier]
[Region Detector]
[Segmentator]
}
component "Quality Assessment" as QA {
[Motion Detector]
[Artifact Detector]
[Position Validator]
[ROI Checker]
[Rotation Checker]
}
database "File System" as FS {
[DICOM Files]
[Model Weights]
}
Client -down-> API : HTTP
API -down-> FS : Read/Write
API -right-> Models : Inference
Models -down-> QA : Quality Metrics
@enduml
```
### Диаграмма классов (основные сущности)
```plantuml
@startuml
!theme plain
class DXAQualityClassifier {
+backbone: str
+num_classes: int
+forward(x: Tensor) -> Tensor
+extract_features(x: Tensor) -> Tensor
}
class DXAQualityModel {
+model: DXAQualityClassifier
+device: str
+predict(images: Tensor) -> (preds, probs)
+train_epoch(loader)
+validate(loader)
}
class DetailedAssessment {
+detect_motion_blur(image) -> Dict
+detect_artifacts(image, segmentation) -> Dict
+check_spine_completeness(segmentation) -> Dict
+check_hip_completeness(segmentation) -> Dict
+check_hip_rotation(segmentation, image) -> Dict
+check_roi_boundaries(segmentation, shape) -> Dict
+generate_quality_report(...) -> Dict
}
class RegionDetector {
+determine_region_from_image(image) -> str
+determine_anatomical_region(dcm_path) -> str
}
DXAQualityModel --> DXAQualityClassifier
DXAQualityModel ..> DetailedAssessment : uses
RegionDetector ..> DetailedAssessment : provides region
@enduml
```
### Диаграмма развертывания
```plantuml
@startuml
!theme plain
skinparam rectangle {
BackgroundColor #White
BorderColor #Black
}
rectangle "Client Layer" {
rectangle "Browser" as Browser
rectangle "Web UI (HTML/JS)" as WebUI
}
rectangle "Application Layer" {
rectangle "FastAPI" as API
rectangle "Uvicorn" as Uvicorn
}
rectangle "ML Pipeline" {
rectangle "DXA Classifier" as Classifier
rectangle "Segmentator" as Segmentator
rectangle "Quality Assessor" as Assessor
}
rectangle "Infrastructure" {
rectangle "CPU/GPU" as Compute
rectangle "File System" as Storage
rectangle "Docker" as Docker
}
Browser -right-> WebUI
WebUI -right-> API
API -right-> Uvicorn
Uvicorn -right-> Classifier
Uvicorn -right-> Segmentator
Classifier -right-> Assessor
Classifier -up-> Compute : PyTorch
Segmentator -up-> Compute : NumPy/SciPy
API -down-> Storage : Models/Data
Compute -up-> Docker
@enduml
```
---
## Потоки данных
```plantuml
@startuml
!theme plain
skinparam ranksep 50
skinparam nodesep 50
node "Input" {
[DICOM File]
}
node "Preprocessing" {
[Normalization]
[3-Channel Conv]
[Resize 224x224]
}
node "ML Models" {
[ResNet18]
[Region Detector]
[Threshold Seg]
}
node "Quality Analysis" {
[Motion Detection]
[Artifact Detection]
[Completeness Check]
[ROI Validation]
}
node "Output" {
[JSON Response]
[XLSX Export]
}
[DICOM File] --> [Normalization]
[Normalization] --> [3-Channel Conv]
[3-Channel Conv] --> [Resize 224x224]
[Resize 224x224] --> [ResNet18]
[Resize 224x224] --> [Region Detector]
[Resize 224x224] --> [Threshold Seg]
[ResNet18] --> [Quality Analysis]
[Region Detector] --> [Quality Analysis]
[Threshold Seg] --> [Quality Analysis]
[Quality Analysis] --> [JSON Response]
[Quality Analysis] --> [XLSX Export]
@enduml
```
---
## Ограничения и планы развития
### Текущие ограничения
1. **Сегментация** — простая пороговая обработка, требует замены на Deep Learning (UNet/TotalSegmentator)
2. **Модель** — бинарная классификация, нужно расширение до многоклассовой для типов нарушений
3. **Dataset** — ~100 исследований, требуется расширение до 500+
4. **F1 Score** — текущий ~0.27, требует улучшения
### Планируемые улучшения
| Компонент | План |
|-----------|------|
| Region Detector | Отдельная модель для детекции региона |
| Segmentator | UNet или TotalSegmentator |
| Quality Classifier | Расширение на 8 классов нарушений |
| Violation Classifier | Отдельная модель для классификации типа нарушения |
| Artifact Detector | CNN для детекции металла/имплантатов |
| Visualization | Grad-CAM для внимания модели |
---
## Заключение
Система DXA Quality Assessment представляет собой полнофункциональный AI-сервис для автоматизированной оценки качества DXA исследований. Архитектура построена на принципах модульности и расширяемости, что позволяет поэтапно улучшать отдельные компоненты.
Основные характеристики:
- ✅ RESTful API с FastAPI
- ✅ Глубокое обучение (ResNet18)
- ✅ Детальная оценка качества с метриками
- ✅ Поддержка нескольких типов нарушений
- ✅ Экспорт в XLSX/CSV
- ✅ Docker контейнеризация

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,37 +1,72 @@
# Core dependencies annotated-doc==0.0.5
torch>=2.0.0 annotated-types==0.7.0
torchvision>=0.15.0 anyio==4.12.1
numpy>=1.24.0 attrs==26.1.0
certifi==2026.7.22
# Image processing click==8.1.8
Pillow>=10.0.0 contourpy==1.3.0
opencv-python-headless>=4.8.0 cycler==0.12.1
et_xmlfile==2.0.0
# Medical imaging exceptiongroup==1.3.1
pydicom>=2.4.0 fastapi==0.128.8
pydicom-seg>=0.4.0 filelock==3.19.1
nibabel>=5.0.0 fonttools==4.60.2
SimpleITK>=2.2.0 fsspec==2025.10.0
h11==0.16.0
# Deep learning / models hf-xet==1.6.0
torchvision>=0.15.0 httpcore==1.0.9
timm>=0.9.0 httpx==0.28.1
huggingface_hub==1.8.0
# Data handling idna==3.18
pandas>=2.0.0 importlib_resources==6.5.2
openpyxl>=3.1.0 Jinja2==3.1.6
joblib==1.5.3
# Visualization jsonschema==3.2.0
matplotlib>=3.7.0 kiwisolver==1.4.7
markdown-it-py==3.0.0
# API / serving MarkupSafe==3.0.3
fastapi>=0.100.0 matplotlib==3.9.4
uvicorn>=0.23.0 mdurl==0.1.2
python-multipart>=0.0.6 monai==1.5.2
mpmath==1.3.0
# Progress bars networkx==3.2.1
tqdm>=4.65.0 nibabel==5.3.3
numpy==1.26.4
# Utils opencv-python-headless==4.11.0.86
scipy>=1.10.0 openpyxl==3.1.5
scikit-learn>=1.3.0 packaging==26.3
pandas==2.3.3
pillow==11.3.0
pydantic==2.13.4
pydantic_core==2.46.4
pydicom==2.4.4
pydicom-seg==0.4.1
Pygments==2.21.0
pyparsing==3.3.2
pyrsistent==0.20.0
python-dateutil==2.9.0.post0
python-multipart==0.0.20
pytz==2026.3.post1
PyYAML==6.0.3
rich==15.0.0
safetensors==0.7.0
scikit-learn==1.6.1
scipy==1.13.1
shellingham==1.5.4
simpleitk==2.5.6
six==1.17.0
starlette==0.49.3
sympy==1.14.0
threadpoolctl==3.7.0
timm==1.0.29
torch==2.8.0
torchvision==0.23.0
TotalSegmentator==2.18.0
tqdm==4.70.0
typer==0.23.2
typing-inspection==0.4.2
typing_extensions==4.16.0
tzdata==2026.3
unicorn==2.1.4
uvicorn==0.39.0
zipp==3.23.1

View File

@ -256,7 +256,20 @@
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
</div> </div>
<div class="p-4"> <div class="p-4 max-h-[80vh] overflow-y-auto">
<!-- Quality Badge -->
<div id="detailBadge" class="mb-4 flex items-center gap-3">
<!-- Populated by JS -->
</div>
<!-- UIDs -->
<div class="mb-4 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Идентификаторы</h4>
<div id="detailUIDs" class="text-xs font-mono space-y-1">
<!-- Populated by JS -->
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Basic Info --> <!-- Basic Info -->
<div> <div>
@ -265,7 +278,7 @@
<!-- Populated by JS --> <!-- Populated by JS -->
</div> </div>
</div> </div>
<!-- Quality Details --> <!-- Quality Details -->
<div> <div>
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Детали качества</h4> <h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Детали качества</h4>
@ -273,33 +286,101 @@
<!-- Populated by JS --> <!-- Populated by JS -->
</div> </div>
</div> </div>
<!-- Metrics --> <!-- Confidence -->
<div class="md:col-span-2"> <div>
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Метрики</h4> <h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Уверенность модели</h4>
<div id="detailMetrics" class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div id="detailConfidence" class="space-y-2">
<!-- Populated by JS --> <!-- Populated by JS -->
</div> </div>
</div> </div>
<!-- Reason --> <!-- Overall -->
<div class="md:col-span-2"> <div>
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Причина</h4> <h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Общая оценка</h4>
<div id="detailReason" class="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg text-sm"> <div id="detailOverall" class="space-y-2 text-sm">
<!-- Populated by JS --> <!-- Populated by JS -->
</div> </div>
</div> </div>
<!-- Visualization --> <!-- Motion Metrics -->
<div class="md:col-span-2" id="detailVizSection"> <div class="md:col-span-2">
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Визуализация</h4> <h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
<div class="flex gap-4"> <i class="fas fa-running mr-1"></i> Движение (Motion)
<div id="detailImage" class="flex-1"> </h4>
<p class="text-xs text-gray-500 mb-1">Оригинал</p> <div id="detailMotion" class="grid grid-cols-2 md:grid-cols-4 gap-3">
</div> <!-- Populated by JS -->
<div id="detailMask" class="flex-1"> </div>
<p class="text-xs text-gray-500 mb-1">Маска ROI</p> </div>
</div>
<!-- Artifacts -->
<div class="md:col-span-2">
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
<i class="fas fa-exclamation-triangle mr-1"></i> Артефакты
</h4>
<div id="detailArtifacts" class="grid grid-cols-2 md:grid-cols-5 gap-3">
<!-- Populated by JS -->
</div>
</div>
<!-- Position -->
<div>
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
<i class="fas fa-crosshairs mr-1"></i> Позиция
</h4>
<div id="detailPosition" class="space-y-2 text-sm">
<!-- Populated by JS -->
</div>
</div>
<!-- ROI -->
<div>
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
<i class="fas fa-crop-alt mr-1"></i> ROI
</h4>
<div id="detailROI" class="space-y-2 text-sm">
<!-- Populated by JS -->
</div>
</div>
<!-- Spine Completeness -->
<div class="md:col-span-2 hidden" id="spineSection">
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
<i class="fas fa-bone mr-1"></i> Позвоночник
</h4>
<div id="detailSpine" class="grid grid-cols-2 md:grid-cols-4 gap-3">
<!-- Populated by JS -->
</div>
</div>
<!-- Hip Completeness -->
<div class="md:col-span-2 hidden" id="hipSection">
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
<i class="fas fa-circle mr-1"></i> Бедро
</h4>
<div id="detailHip" class="grid grid-cols-2 md:grid-cols-4 gap-3">
<!-- Populated by JS -->
</div>
</div>
</div>
<!-- Reason -->
<div class="mt-6">
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Заключение</h4>
<div id="detailReason" class="p-4 rounded-lg text-base">
<!-- Populated by JS -->
</div>
</div>
<!-- Visualization -->
<div class="mt-6" id="detailVizSection">
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Визуализация</h4>
<div class="flex gap-4">
<div id="detailImage" class="flex-1">
<p class="text-xs text-gray-500 mb-1">Оригинал</p>
</div>
<div id="detailMask" class="flex-1">
<p class="text-xs text-gray-500 mb-1">Маска ROI</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -359,74 +359,265 @@ document.getElementById('closeDetail')?.addEventListener('click', () => {
// View detailed analysis for a file // View detailed analysis for a file
async function viewDetail(result) { async function viewDetail(result) {
detailSection.classList.remove('hidden'); detailSection.classList.remove('hidden');
// Populate basic info // Region label helper
const regionLabel = result.anatomical_region === 'spine' ? 'Позвоночник' : const getRegionLabel = (region) => {
result.anatomical_region === 'hip_left' ? 'Бедро левое' : const labels = {
result.anatomical_region === 'hip_right' ? 'Бедро правое' : 'spine': 'Позвоночник',
result.anatomical_region === 'hip' ? 'Бедро' : result.anatomical_region || '—'; 'hip_left': 'Бедро левое',
'hip_right': 'Бедро правое',
document.getElementById('detailBasic').innerHTML = ` 'hip': 'Бедро (общее)',
<div class="flex justify-between"><span class="text-gray-500">Файл:</span><span class="font-medium">${result.filename || '—'}</span></div> 'unknown': 'Неизвестно'
<div class="flex justify-between"><span class="text-gray-500">Регион:</span><span class="font-medium">${regionLabel}</span></div> };
<div class="flex justify-between"><span class="text-gray-500">UID исследования:</span><span class="font-medium text-xs">${(result.study_uid || '').substring(0, 20)}...</span></div> return labels[region] || region || '—';
`; };
// Populate quality details // Violation type label
const getViolationLabel = (type) => {
const labels = {
'correct': 'Корректно',
'position_error': 'Ошибка позиционирования',
'artifact_motion': 'Артефакт движения',
'artifact_other': 'Другие артефакты',
'labeling_error': 'Ошибка маркировки',
'incomplete_view': 'Неполный вид',
'roi_error': 'Ошибка ROI',
'rotation': 'Неправильный поворот'
};
return labels[type] || type || '—';
};
// Quality badge
const qualityClass = result.quality_class === 0 ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400' : const qualityClass = result.quality_class === 0 ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400' :
result.quality_class === 1 ? 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400' : result.quality_class === 1 ? 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400' :
'bg-gray-100 text-gray-800'; 'bg-gray-100 text-gray-800';
const qualityLabel = result.quality_class === 0 ? 'OK' : result.quality_class === 1 ? 'Нарушение' : 'Ошибка';
const violationType = result.violation_type || (result.quality_class === 1 ? 'quality_violation_detected' : 'correct');
const reason = result.reason || (result.quality_class === 0 ? 'Качество соответствует норме' : 'Нарушение качества'); document.getElementById('detailBadge').innerHTML = `
<span class="px-3 py-1.5 rounded-lg text-sm font-medium ${qualityClass}">
document.getElementById('detailQuality').innerHTML = ` ${qualityLabel}
<div class="flex justify-between"><span class="text-gray-500">Класс:</span><span class="px-2 py-0.5 rounded text-xs font-medium ${qualityClass}">${result.quality_class === 0 ? 'OK' : 'Нарушение'}</span></div> </span>
<div class="flex justify-between"><span class="text-gray-500">Тип нарушения:</span><span class="font-medium">${violationType}</span></div> <span class="text-sm text-gray-600 dark:text-gray-400">${result.quality_label || ''}</span>
<div class="flex justify-between"><span class="text-gray-500">Уверенность:</span><span class="font-medium">${result.confidence ? (result.confidence * 100).toFixed(1) + '%' : '—'}</span></div>
<div class="flex justify-between"><span class="text-gray-500">Вид:</span><span class="font-medium">${result.view_quality || 'unknown'}</span></div>
`; `;
// Populate metrics // UIDs
document.getElementById('detailUIDs').innerHTML = `
<div class="flex justify-between"><span class="text-gray-500">Study UID:</span><span class="text-gray-900 dark:text-gray-200">${result.study_uid || '—'}</span></div>
<div class="flex justify-between"><span class="text-gray-500">Image UID:</span><span class="text-gray-900 dark:text-gray-200">${result.image_uid || '—'}</span></div>
`;
// Basic info
document.getElementById('detailBasic').innerHTML = `
<div class="flex justify-between"><span class="text-gray-500">Файл:</span><span class="font-medium">${result.filename || '—'}</span></div>
<div class="flex justify-between"><span class="text-gray-500">Регион:</span><span class="font-medium">${getRegionLabel(result.anatomical_region)}</span></div>
<div class="flex justify-between"><span class="text-gray-500">Вид снимка:</span><span class="font-medium">${result.view_quality || 'unknown'}</span></div>
`;
// Quality details
document.getElementById('detailQuality').innerHTML = `
<div class="flex justify-between"><span class="text-gray-500">Тип нарушения:</span><span class="font-medium">${getViolationLabel(result.violation_type)}</span></div>
<div class="flex justify-between"><span class="text-gray-500">Класс:</span><span class="font-medium">${result.quality_class}</span></div>
`;
// Confidence
const confCorrect = result.confidence_per_class?.correct || 0;
const confViolation = result.confidence_per_class?.violation || 0;
document.getElementById('detailConfidence').innerHTML = `
<div class="space-y-2">
<div class="flex justify-between text-sm"><span class="text-gray-500">Верный:</span><span class="font-medium text-green-600">${(confCorrect * 100).toFixed(1)}%</span></div>
<div class="w-full bg-gray-200 dark:bg-gray-600 rounded-full h-2">
<div class="bg-green-500 h-2 rounded-full" style="width: ${confCorrect * 100}%"></div>
</div>
<div class="flex justify-between text-sm"><span class="text-gray-500">Нарушение:</span><span class="font-medium text-red-600">${(confViolation * 100).toFixed(1)}%</span></div>
<div class="w-full bg-gray-200 dark:bg-gray-600 rounded-full h-2">
<div class="bg-red-500 h-2 rounded-full" style="width: ${confViolation * 100}%"></div>
</div>
</div>
`;
// Overall quality
const overallClass = result.overall_quality === 'GOOD' ? 'text-green-600' :
result.overall_quality === 'FAIR' ? 'text-yellow-600' :
result.overall_quality === 'POOR' ? 'text-red-600' : 'text-gray-600';
const severityClass = result.severity === 'LOW' ? 'bg-green-100 text-green-800 dark:bg-green-900/30' :
result.severity === 'MEDIUM' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30' :
result.severity === 'HIGH' ? 'bg-red-100 text-red-800 dark:bg-red-900/30' : 'bg-gray-100';
document.getElementById('detailOverall').innerHTML = `
<div class="flex justify-between"><span class="text-gray-500">Качество:</span><span class="font-medium ${overallClass}">${result.overall_quality || '—'}</span></div>
<div class="flex justify-between items-center"><span class="text-gray-500">Серьёзность:</span><span class="px-2 py-0.5 rounded text-xs ${severityClass}">${result.severity || '—'}</span></div>
`;
// Motion metrics
const metrics = result.metrics || {}; const metrics = result.metrics || {};
const motion = metrics.motion || {}; const motion = metrics.motion || {};
const artifacts = metrics.artifacts || {}; const blurLaplacian = motion.blur_laplacian?.toFixed(4) || '—';
const roi = metrics.roi_check || {}; const blurFFT = motion.blur_fft?.toFixed(4) || '—';
document.getElementById('detailMetrics').innerHTML = ` document.getElementById('detailMotion').innerHTML = `
<div class="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"> <div class="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="text-xs text-gray-500 mb-1">Движение</div> <div class="text-xs text-gray-500 mb-1">Движение</div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full ${motion.motion_detected ? 'bg-red-500' : 'bg-green-500'}"></div> <div class="w-2 h-2 rounded-full ${motion.motion_detected ? 'bg-red-500' : 'bg-green-500'}"></div>
<span class="font-medium">${motion.motion_detected ? 'Обнаружено' : 'Нет'}</span> <span class="font-medium">${motion.motion_detected ? 'Да' : 'Нет'}</span>
</div> </div>
${motion.severity ? `<div class="text-xs text-gray-500">Severity: ${motion.severity}</div>` : ''}
</div> </div>
<div class="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"> <div class="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="text-xs text-gray-500 mb-1">Артефакты</div> <div class="text-xs text-gray-500 mb-1">Blur Laplacian</div>
<div class="flex items-center gap-2"> <span class="font-mono text-sm">${blurLaplacian}</span>
<div class="w-2 h-2 rounded-full ${artifacts.any_detected ? 'bg-red-500' : 'bg-green-500'}"></div> ${motion.is_blurred_laplacian ? '<span class="text-xs text-red-500">размыто</span>' : ''}
<span class="font-medium">${artifacts.any_detected ? 'Обнаружены' : 'Нет'}</span>
</div>
${artifacts.metal_detected ? `<div class="text-xs text-gray-500">Металл: да</div>` : ''}
${artifacts.implant_detected ? `<div class="text-xs text-gray-500">Имплантат: да</div>` : ''}
</div> </div>
<div class="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"> <div class="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="text-xs text-gray-500 mb-1">ROI</div> <div class="text-xs text-gray-500 mb-1">Blur FFT</div>
<span class="font-mono text-sm">${blurFFT}</span>
${motion.is_blurred_fft ? '<span class="text-xs text-red-500">размыто</span>' : ''}
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="text-xs text-gray-500 mb-1">Дублирование краёв</div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full ${roi.valid === false ? 'bg-red-500' : 'bg-green-500'}"></div> <div class="w-2 h-2 rounded-full ${motion.edge_duplication ? 'bg-red-500' : 'bg-green-500'}"></div>
<span class="font-medium">${roi.valid === false ? 'Проблема' : 'OK'}</span> <span class="font-medium">${motion.edge_duplication ? 'Да' : 'Нет'}</span>
</div> </div>
</div> </div>
`; `;
// Populate reason // Artifacts
const reasonClass = result.quality_class === 0 ? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200' : const artifacts = metrics.artifacts || {};
'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'; document.getElementById('detailArtifacts').innerHTML = `
<div class="p-2 bg-gray-50 dark:bg-gray-700 rounded-lg text-center">
<div class="text-xs text-gray-500">Металл</div>
<div class="flex items-center justify-center gap-1 mt-1">
<div class="w-2 h-2 rounded-full ${artifacts.metal_detected ? 'bg-red-500' : 'bg-green-500'}"></div>
<span class="text-sm font-medium">${artifacts.metal_detected ? 'Да' : 'Нет'}</span>
</div>
</div>
<div class="p-2 bg-gray-50 dark:bg-gray-700 rounded-lg text-center">
<div class="text-xs text-gray-500">Имплантат</div>
<div class="flex items-center justify-center gap-1 mt-1">
<div class="w-2 h-2 rounded-full ${artifacts.implant_detected ? 'bg-red-500' : 'bg-green-500'}"></div>
<span class="text-sm font-medium">${artifacts.implant_detected ? 'Да' : 'Нет'}</span>
</div>
</div>
<div class="p-2 bg-gray-50 dark:bg-gray-700 rounded-lg text-center">
<div class="text-xs text-gray-500">Цемент</div>
<div class="flex items-center justify-center gap-1 mt-1">
<div class="w-2 h-2 rounded-full ${artifacts.cement_detected ? 'bg-red-500' : 'bg-green-500'}"></div>
<span class="text-sm font-medium">${artifacts.cement_detected ? 'Да' : 'Нет'}</span>
</div>
</div>
<div class="p-2 bg-gray-50 dark:bg-gray-700 rounded-lg text-center">
<div class="text-xs text-gray-500">Кальцификаты</div>
<div class="flex items-center justify-center gap-1 mt-1">
<div class="w-2 h-2 rounded-full ${artifacts.calcification_detected ? 'bg-yellow-500' : 'bg-green-500'}"></div>
<span class="text-sm font-medium">${artifacts.calcification_detected ? 'Да' : 'Нет'}</span>
</div>
</div>
<div class="p-2 bg-gray-50 dark:bg-gray-700 rounded-lg text-center">
<div class="text-xs text-gray-500">Локальные дефекты</div>
<div class="flex items-center justify-center gap-1 mt-1">
<div class="w-2 h-2 rounded-full ${artifacts.local_defects?.length > 0 ? 'bg-red-500' : 'bg-green-500'}"></div>
<span class="text-sm font-medium">${artifacts.local_defects?.length || 0}</span>
</div>
</div>
`;
// Position
const position = metrics.position || {};
const posValid = position.valid !== false;
document.getElementById('detailPosition').innerHTML = `
<div class="flex justify-between"><span class="text-gray-500">Центр:</span><span class="font-mono">${position.center_pixels ? `[${position.center_pixels[0]}, ${position.center_pixels[1]}]` : '—'}</span></div>
<div class="flex justify-between"><span class="text-gray-500">Отклонение:</span><span class="font-mono">${position.deviation?.toFixed(4) || '—'}</span></div>
<div class="flex justify-between"><span class="text-gray-500">Размер объекта:</span><span class="font-mono">${position.object_size?.toFixed(4) || '—'}</span></div>
<div class="flex justify-between items-center"><span class="text-gray-500">Валидность:</span><span class="px-2 py-0.5 rounded text-xs ${posValid ? 'bg-green-100 text-green-800 dark:bg-green-900/30' : 'bg-red-100 text-red-800 dark:bg-red-900/30'}">${posValid ? 'OK' : 'Ошибка'}</span></div>
`;
// ROI
const roi = metrics.roi_check || {};
const roiValid = roi.valid !== false;
document.getElementById('detailROI').innerHTML = `
<div class="flex justify-between"><span class="text-gray-500">Валидность:</span><span class="px-2 py-0.5 rounded text-xs ${roiValid ? 'bg-green-100 text-green-800 dark:bg-green-900/30' : 'bg-red-100 text-red-800 dark:bg-red-900/30'}">${roiValid ? 'OK' : 'Проблема'}</span></div>
<div class="flex justify-between"><span class="text-gray-500">Размер:</span><span class="font-mono">${roi.size ? `${roi.size.width}x${roi.size.height}` : '—'}</span></div>
<div class="flex justify-between"><span class="text-gray-500">BBox:</span><span class="font-mono text-xs">${roi.bounding_box ? `x:${roi.bounding_box.x_min}-${roi.bounding_box.x_max}, y:${roi.bounding_box.y_min}-${roi.bounding_box.y_max}` : '—'}</span></div>
${roi.issues?.length ? `<div class="mt-2"><span class="text-gray-500 text-xs">Проблемы:</span><ul class="text-xs text-red-600 list-disc list-inside">${roi.issues.map(i => `<li>${i}</li>`).join('')}</ul></div>` : ''}
`;
// Spine completeness
const spine = result.spine_completeness || {};
const spineSection = document.getElementById('spineSection');
if (spine && Object.keys(spine).length > 0) {
spineSection.classList.remove('hidden');
document.getElementById('detailSpine').innerHTML = `
<div class="p-2 bg-gray-50 dark:bg-gray-700 rounded-lg text-center">
<div class="text-xs text-gray-500">Позвонков</div>
<span class="font-medium">${spine.vertebrae_count || '—'}</span>
</div>
<div class="p-2 bg-gray-50 dark:bg-gray-700 rounded-lg text-center">
<div class="text-xs text-gray-500">Полный вид</div>
<div class="flex items-center justify-center gap-1 mt-1">
<div class="w-2 h-2 rounded-full ${spine.is_complete ? 'bg-green-500' : 'bg-red-500'}"></div>
<span class="text-sm">${spine.is_complete ? 'Да' : 'Нет'}</span>
</div>
</div>
<div class="p-2 bg-gray-50 dark:bg-gray-700 rounded-lg text-center">
<div class="text-xs text-gray-500">Валидность</div>
<div class="flex items-center justify-center gap-1 mt-1">
<div class="w-2 h-2 rounded-full ${spine.valid ? 'bg-green-500' : 'bg-red-500'}"></div>
<span class="text-sm">${spine.valid ? 'OK' : 'Ошибка'}</span>
</div>
</div>
<div class="p-2 bg-gray-50 dark:bg-gray-700 rounded-lg text-center">
<div class="text-xs text-gray-500">Проблемы</div>
<span class="text-sm text-red-600">${spine.issues?.length || 0}</span>
</div>
`;
} else {
spineSection.classList.add('hidden');
}
// Hip completeness
const hip = result.hip_completeness || {};
const hipRotation = result.hip_rotation || {};
const hipSection = document.getElementById('hipSection');
if ((hip && Object.keys(hip).length > 0) || (hipRotation && Object.keys(hipRotation).length > 0)) {
hipSection.classList.remove('hidden');
document.getElementById('detailHip').innerHTML = `
<div class="p-2 bg-gray-50 dark:bg-gray-700 rounded-lg text-center">
<div class="text-xs text-gray-500">Полный вид</div>
<div class="flex items-center justify-center gap-1 mt-1">
<div class="w-2 h-2 rounded-full ${hip.is_complete ? 'bg-green-500' : 'bg-red-500'}"></div>
<span class="text-sm">${hip.is_complete ? 'Да' : 'Нет'}</span>
</div>
</div>
<div class="p-2 bg-gray-50 dark:bg-gray-700 rounded-lg text-center">
<div class="text-xs text-gray-500">Соотношение сторон</div>
<span class="font-mono text-sm">${hip.aspect_ratio?.toFixed(2) || '—'}</span>
</div>
<div class="p-2 bg-gray-50 dark:bg-gray-700 rounded-lg text-center">
<div class="text-xs text-gray-500">Угол поворота</div>
<span class="font-mono text-sm">${hipRotation.angle ? hipRotation.angle.toFixed(1) + '°' : '—'}</span>
</div>
<div class="p-2 bg-gray-50 dark:bg-gray-700 rounded-lg text-center">
<div class="text-xs text-gray-500">Валидность</div>
<div class="flex items-center justify-center gap-1 mt-1">
<div class="w-2 h-2 rounded-full ${(hip.valid && hipRotation.valid) ? 'bg-green-500' : 'bg-red-500'}"></div>
<span class="text-sm">${(hip.valid && hipRotation.valid) ? 'OK' : 'Ошибка'}</span>
</div>
</div>
`;
} else {
hipSection.classList.add('hidden');
}
// Reason
const reasonClass = result.quality_class === 0 ? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200 border border-green-200 dark:border-green-800' :
'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200 border border-red-200 dark:border-red-800';
document.getElementById('detailReason').innerHTML = ` document.getElementById('detailReason').innerHTML = `
<span class="${reasonClass} px-3 py-2 rounded-lg block">${reason}</span> <span class="${reasonClass} px-4 py-3 rounded-lg block">
<i class="fas fa-${result.quality_class === 0 ? 'check-circle' : 'exclamation-circle'} mr-2"></i>
${result.reason || (result.quality_class === 0 ? 'Качество соответствует норме' : 'Нарушение качества')}
</span>
`; `;
// Scroll to detail // Scroll to detail
detailSection.scrollIntoView({ behavior: 'smooth' }); detailSection.scrollIntoView({ behavior: 'smooth' });
} }

View File

@ -203,7 +203,7 @@ def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--data-root', default='dataset_hack/НД_для_обучения') parser.add_argument('--data-root', default='dataset_hack/НД_для_обучения')
parser.add_argument('--annotation', default='dataset_hack/НД_для_обучения/разметка.xlsx') parser.add_argument('--annotation', default='dataset_hack/НД_для_обучения/разметка.xlsx')
parser.add_argument('--model-type', default='quality', choices=['quality', 'region', 'violation']) parser.add_argument('--model-type', default='violation', choices=['quality', 'region', 'violation'])
parser.add_argument('--epochs', type=int, default=15) parser.add_argument('--epochs', type=int, default=15)
parser.add_argument('--batch-size', type=int, default=8) parser.add_argument('--batch-size', type=int, default=8)
parser.add_argument('--lr', type=float, default=1e-4) parser.add_argument('--lr', type=float, default=1e-4)