develop - hack_2026
|
|
@ -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 контейнеризация
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 18 KiB |
109
requirements.txt
|
|
@ -1,37 +1,72 @@
|
|||
# Core dependencies
|
||||
torch>=2.0.0
|
||||
torchvision>=0.15.0
|
||||
numpy>=1.24.0
|
||||
|
||||
# Image processing
|
||||
Pillow>=10.0.0
|
||||
opencv-python-headless>=4.8.0
|
||||
|
||||
# Medical imaging
|
||||
pydicom>=2.4.0
|
||||
pydicom-seg>=0.4.0
|
||||
nibabel>=5.0.0
|
||||
SimpleITK>=2.2.0
|
||||
|
||||
# Deep learning / models
|
||||
torchvision>=0.15.0
|
||||
timm>=0.9.0
|
||||
|
||||
# Data handling
|
||||
pandas>=2.0.0
|
||||
openpyxl>=3.1.0
|
||||
|
||||
# Visualization
|
||||
matplotlib>=3.7.0
|
||||
|
||||
# API / serving
|
||||
fastapi>=0.100.0
|
||||
uvicorn>=0.23.0
|
||||
python-multipart>=0.0.6
|
||||
|
||||
# Progress bars
|
||||
tqdm>=4.65.0
|
||||
|
||||
# Utils
|
||||
scipy>=1.10.0
|
||||
scikit-learn>=1.3.0
|
||||
annotated-doc==0.0.5
|
||||
annotated-types==0.7.0
|
||||
anyio==4.12.1
|
||||
attrs==26.1.0
|
||||
certifi==2026.7.22
|
||||
click==8.1.8
|
||||
contourpy==1.3.0
|
||||
cycler==0.12.1
|
||||
et_xmlfile==2.0.0
|
||||
exceptiongroup==1.3.1
|
||||
fastapi==0.128.8
|
||||
filelock==3.19.1
|
||||
fonttools==4.60.2
|
||||
fsspec==2025.10.0
|
||||
h11==0.16.0
|
||||
hf-xet==1.6.0
|
||||
httpcore==1.0.9
|
||||
httpx==0.28.1
|
||||
huggingface_hub==1.8.0
|
||||
idna==3.18
|
||||
importlib_resources==6.5.2
|
||||
Jinja2==3.1.6
|
||||
joblib==1.5.3
|
||||
jsonschema==3.2.0
|
||||
kiwisolver==1.4.7
|
||||
markdown-it-py==3.0.0
|
||||
MarkupSafe==3.0.3
|
||||
matplotlib==3.9.4
|
||||
mdurl==0.1.2
|
||||
monai==1.5.2
|
||||
mpmath==1.3.0
|
||||
networkx==3.2.1
|
||||
nibabel==5.3.3
|
||||
numpy==1.26.4
|
||||
opencv-python-headless==4.11.0.86
|
||||
openpyxl==3.1.5
|
||||
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
|
||||
|
|
|
|||
|
|
@ -256,7 +256,20 @@
|
|||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</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">
|
||||
<!-- Basic Info -->
|
||||
<div>
|
||||
|
|
@ -265,7 +278,7 @@
|
|||
<!-- Populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Quality Details -->
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Детали качества</h4>
|
||||
|
|
@ -273,33 +286,101 @@
|
|||
<!-- Populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metrics -->
|
||||
<div class="md:col-span-2">
|
||||
<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">
|
||||
|
||||
<!-- Confidence -->
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Уверенность модели</h4>
|
||||
<div id="detailConfidence" class="space-y-2">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reason -->
|
||||
<div class="md:col-span-2">
|
||||
<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">
|
||||
|
||||
<!-- Overall -->
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Общая оценка</h4>
|
||||
<div id="detailOverall" class="space-y-2 text-sm">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Visualization -->
|
||||
<div class="md:col-span-2" 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>
|
||||
|
||||
<!-- Motion Metrics -->
|
||||
<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-running mr-1"></i> Движение (Motion)
|
||||
</h4>
|
||||
<div id="detailMotion" class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<!-- Populated by JS -->
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -359,74 +359,265 @@ document.getElementById('closeDetail')?.addEventListener('click', () => {
|
|||
// View detailed analysis for a file
|
||||
async function viewDetail(result) {
|
||||
detailSection.classList.remove('hidden');
|
||||
|
||||
// Populate basic info
|
||||
const regionLabel = result.anatomical_region === 'spine' ? 'Позвоночник' :
|
||||
result.anatomical_region === 'hip_left' ? 'Бедро левое' :
|
||||
result.anatomical_region === 'hip_right' ? 'Бедро правое' :
|
||||
result.anatomical_region === 'hip' ? 'Бедро' : result.anatomical_region || '—';
|
||||
|
||||
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">${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>
|
||||
`;
|
||||
|
||||
// Populate quality details
|
||||
|
||||
// Region label helper
|
||||
const getRegionLabel = (region) => {
|
||||
const labels = {
|
||||
'spine': 'Позвоночник',
|
||||
'hip_left': 'Бедро левое',
|
||||
'hip_right': 'Бедро правое',
|
||||
'hip': 'Бедро (общее)',
|
||||
'unknown': 'Неизвестно'
|
||||
};
|
||||
return labels[region] || region || '—';
|
||||
};
|
||||
|
||||
// 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' :
|
||||
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';
|
||||
|
||||
const violationType = result.violation_type || (result.quality_class === 1 ? 'quality_violation_detected' : 'correct');
|
||||
const reason = result.reason || (result.quality_class === 0 ? 'Качество соответствует норме' : 'Нарушение качества');
|
||||
|
||||
document.getElementById('detailQuality').innerHTML = `
|
||||
<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>
|
||||
<div class="flex justify-between"><span class="text-gray-500">Тип нарушения:</span><span class="font-medium">${violationType}</span></div>
|
||||
<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>
|
||||
const qualityLabel = result.quality_class === 0 ? 'OK' : result.quality_class === 1 ? 'Нарушение' : 'Ошибка';
|
||||
|
||||
document.getElementById('detailBadge').innerHTML = `
|
||||
<span class="px-3 py-1.5 rounded-lg text-sm font-medium ${qualityClass}">
|
||||
${qualityLabel}
|
||||
</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">${result.quality_label || ''}</span>
|
||||
`;
|
||||
|
||||
// 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 motion = metrics.motion || {};
|
||||
const artifacts = metrics.artifacts || {};
|
||||
const roi = metrics.roi_check || {};
|
||||
|
||||
document.getElementById('detailMetrics').innerHTML = `
|
||||
const blurLaplacian = motion.blur_laplacian?.toFixed(4) || '—';
|
||||
const blurFFT = motion.blur_fft?.toFixed(4) || '—';
|
||||
|
||||
document.getElementById('detailMotion').innerHTML = `
|
||||
<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="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>
|
||||
${motion.severity ? `<div class="text-xs text-gray-500">Severity: ${motion.severity}</div>` : ''}
|
||||
</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="w-2 h-2 rounded-full ${artifacts.any_detected ? 'bg-red-500' : 'bg-green-500'}"></div>
|
||||
<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 class="text-xs text-gray-500 mb-1">Blur Laplacian</div>
|
||||
<span class="font-mono text-sm">${blurLaplacian}</span>
|
||||
${motion.is_blurred_laplacian ? '<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">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="w-2 h-2 rounded-full ${roi.valid === false ? 'bg-red-500' : 'bg-green-500'}"></div>
|
||||
<span class="font-medium">${roi.valid === false ? 'Проблема' : 'OK'}</span>
|
||||
<div class="w-2 h-2 rounded-full ${motion.edge_duplication ? 'bg-red-500' : 'bg-green-500'}"></div>
|
||||
<span class="font-medium">${motion.edge_duplication ? 'Да' : 'Нет'}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Populate reason
|
||||
const reasonClass = result.quality_class === 0 ? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200' :
|
||||
'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200';
|
||||
|
||||
// Artifacts
|
||||
const artifacts = metrics.artifacts || {};
|
||||
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 = `
|
||||
<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
|
||||
detailSection.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ def main():
|
|||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--data-root', default='dataset_hack/НД_для_обучения')
|
||||
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('--batch-size', type=int, default=8)
|
||||
parser.add_argument('--lr', type=float, default=1e-4)
|
||||
|
|
|
|||