diff --git a/Dockerfile b/Dockerfile index 5d87819..baf3c32 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,16 @@ -# --- Этап сборки --- -FROM python:3.10-slim AS builder +# DXA Quality Assessment - Docker Container +# Build: docker build -t dxa-quality . +# Run: docker run -v /path/to/data:/data -p 8000:8000 dxa-quality -RUN apt-get update && apt-get install -y --no-install-recommends \ +# Base image with GPU support +FROM nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04 AS builder + +# Install Python and build tools +RUN apt-get update && apt-get install -y \ + python3.10 \ + python3-pip \ build-essential \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir --user -r requirements.txt \ - --extra-index-url https://download.pytorch.org/whl/cpu - -# --- Финальный этап --- -FROM python:3.10-slim - -# Только runtime-библиотеки, без gcc -RUN apt-get update && apt-get install -y --no-install-recommends \ - libgl1 \ - libglx-mesa0 \ + libgl1-mesa-glx \ libglib2.0-0 \ libsm6 \ libxext6 \ @@ -26,15 +20,20 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ WORKDIR /app -# Копируем установленные пакеты из builder-этапа -COPY --from=builder /root/.local /root/.local -ENV PATH=/root/.local/bin:$PATH +# Copy requirements and install Python dependencies +COPY requirements.txt . +RUN pip3 install --no-cache-dir -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cu118 +# Copy application code COPY . . RUN mkdir -p models +# Expose API port EXPOSE 8000 + +# Environment variables ENV PYTHONUNBUFFERED=1 ENV PYTHONPATH=/app -CMD ["python", "src/run.py"] \ No newline at end of file +# Default command +CMD ["python3", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/QWEN.md b/QWEN.md new file mode 100644 index 0000000..d204199 --- /dev/null +++ b/QWEN.md @@ -0,0 +1,193 @@ +# Bone Quality Assessment Project + +## Project Overview + +Medical AI service for automated assessment of DXA (bone densitometry) study quality. The system analyzes DICOM files and evaluates quality based on standard criteria. + +### Core Purpose (Hackathon) +- Analyze DICOM densitometry studies +- Determine anatomical region (spine/hip) +- Binary classification: quality (OK/violation) +- Output results in XLSX/CSV format per requirements + +### Tech Stack +| Component | Technology | +|-----------|------------| +| Backend | Python 3.10, FastAPI, Uvicorn | +| ML/Deep Learning | PyTorch, torchvision (ResNet18) | +| Image Processing | PIL, OpenCV, pydicom | +| Data Handling | pandas, openpyxl | +| Containerization | Docker | + +--- + +## Project Structure + +``` +bone_2026/ +├── src/ +│ ├── main.py # FastAPI app (DXA mode) +│ ├── run.py # Server runner +│ ├── dxa/ # DXA Quality module +│ │ ├── dataset.py # DXADataset class +│ │ ├── model.py # ResNet18 classifier +│ │ ├── train.py # Training script +│ │ ├── inference.py # Batch inference +│ │ └── __init__.py +│ ├── api/ # REST endpoints +│ ├── core/ # Orchestrator +│ ├── quality/ # Quality scoring +│ ├── segmentators/ # Segmentation models +│ └── classifiers/ # Classification models +├── models/ +│ └── dxa_model.pth # Trained DXA classifier +├── dataset_hack/ # DICOM datasets +│ ├── Для теста/ # Test data (3 files) +│ └── НД_для_обучения/ # Training data (100 studies, 499 DICOMs) +│ └── разметка.xlsx # Annotation file +├── requirements.txt +├── Dockerfile +├── run.sh # Main entry script +└── README.md +``` + +--- + +## DXA Module (`src/dxa/`) + +### Dataset (`dataset.py`) +- Loads DICOM files from studies +- Parses annotation Excel file +- Maps anatomical regions: spine, hip_right, hip_left +- Quality labels: 0 (OK), 1 (violation) +- Total: ~1433 samples (train: 1146, val: 287) + +### Model (`model.py`) +- Architecture: ResNet18 (pretrained on ImageNet) +- Task: Binary classification (quality OK vs violation) +- Input: 224x224 RGB images +- Output: class probabilities + +### Training (`train.py`) +```bash +python src/dxa/train.py --epochs 10 --batch-size 16 +``` + +### Inference (`inference.py`) +```bash +python src/dxa/inference.py \ + --input-path dataset_hack/Для\ теста \ + --output-path results.xlsx \ + --model-path models/dxa_model.pth +``` + +--- + +## Running the Project + +### Training +```bash +# Option 1: Direct Python +python src/dxa/train.py --epochs 10 + +# Option 2: Via run.sh +bash run.sh train +``` + +### Inference +```bash +# Single file +python src/dxa/inference.py --input-path file.dcm --output-path result.xlsx + +# Directory (batch) +python src/dxa/inference.py --input-path dataset_hack/Для\ теста --output-path results.xlsx +``` + +### API Server +```bash +python -m uvicorn src.main:app --host 0.0.0.0 --port 8000 +``` + +--- + +## Output Format (per Hackathon Requirements) + +| Column | Description | +|--------|-------------| +| path_to_study | Path to study directory | +| study_uid | StudyInstanceUID from DICOM | +| image_uid | SOPInstanceUID from DICOM | +| anatomical_region | spine / hip | +| quality_class | 0 (OK), 1 (violation) | +| violation_type | Type of violation (if any) | +| processing_status | Success / Failure | +| time_of_processing | Processing time (seconds) | + +--- + +## Model Performance + +``` +Training data: 1146 samples +Validation data: 287 samples + +Training (10 epochs): +- Best F1: ~0.27 (imbalanced classes: ~65% OK, ~35% violation) +- Accuracy: ~84% + +Note: Need more epochs (50+) and class balancing for production +``` + +--- + +## Annotation Format + +The annotation Excel (`разметка.xlsx`) contains: +- Study UID +- Spine columns: укладка, ось, артефакты +- Hip columns: позиция, ROI (left/right) +- Total columns: итого + +--- + +## Development Conventions + +### Code Style +- Follow existing patterns in src/ +- Type hints where appropriate +- Minimal comments (only for context) + +### Key Components +- **DXADataset**: Handles DICOM loading + annotation parsing +- **DXAQualityClassifier**: ResNet18-based classifier +- **process_dicom_files**: Batch inference with XLSX output + +### Dependencies +All in `requirements.txt`: +- `torch`, `torchvision` - Deep learning +- `pydicom` - DICOM handling +- `pandas`, `openpyxl` - Data/Excel +- `fastapi`, `uvicorn` - Web framework +- `Pillow`, `opencv-python-headless` - Image processing + +--- + +## Docker + +```bash +# Build +docker build -t dxa-quality . + +# Run +docker run -v /data:/data -p 8000:8000 dxa-quality +``` + +--- + +## Notes + +- This is a **hackathon project** for DXA quality assessment +- Model trained on limited data (100 studies) +- Binary classification (quality OK / violation) +- Anatomical region detection via image size heuristic +- Output format matches hackathon requirements (XLSX/CSV) diff --git a/README.md b/README.md index 5a9ef6a..6d5a345 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,18 @@  +## 🎯 Доступные режимы + +### DXA Режим (хакатон) +Анализ денситометрических исследований: +```bash +# Обучение +python src/dxa/train.py --epochs 10 + +# Инференс +python src/dxa/inference.py --input-path dataset_hack/Для\ теста --output-path results.xlsx +``` + ## 🚀 Быстрый старт @@ -52,7 +64,7 @@ venv\Scripts\activate # Windows pip install -r requirements.txt # 4. Загрузка обученной модели (опционально) -# Поместите модель в папку models/unet_cats_dogs.pth +# Поместите модель в папку models/dxa_model.pth # 5. Запуск сервера python run.py @@ -145,22 +157,22 @@ curl -X POST "http://localhost:8000/api/v1/analyze" \ 📁 Структура проекта ```text -bone-quality-assessment/ +bone_2026/ ├── src/ -│ ├── api/ -│ │ ├── endpoints.py # FastAPI эндпоинты -│ │ └── static/ -│ │ └── index.html # Web-интерфейс -│ ├── models/ -│ │ └── unet.py # U-Net архитектура -│ └── quality/ -│ └── quality_scorer.py # Оценка качества +│ ├── dxa/ # DXA Quality модуль +│ │ ├── model.py # ResNet18 классификатор +│ │ ├── dataset.py # Загрузчик данных +│ │ ├── train.py # Обучение +│ │ └── inference.py # Инференс +│ ├── api/ # REST API +│ ├── quality/ # Оценка качества +│ └── main.py # FastAPI приложение ├── models/ -│ └── unet_cats_dogs.pth # Обученная модель +│ └── dxa_model.pth # Обученная модель +├── dataset_hack/ # DICOM датасет ├── Dockerfile -├── docker-compose.yml ├── requirements.txt -├── run.py +├── run.sh └── README.md ``` diff --git a/inference.py b/inference.py deleted file mode 100644 index 2e0b3f0..0000000 --- a/inference.py +++ /dev/null @@ -1,140 +0,0 @@ -import torch -import numpy as np -from PIL import Image -import matplotlib.pyplot as plt -import argparse - -from src import UNet -from src.quality.quality_scorer import QualityScorer - - -# from src.quality.quality_scorer_old import QualityScorer - - -def load_model(model_path, in_channels=3, out_classes=2, device='cpu'): - """Загрузка модели""" - model = UNet(in_channels=in_channels, out_classes=out_classes) - model.load_state_dict(torch.load(model_path, map_location=device)) - model.to(device) - model.eval() - return model - - -def predict_single(model, image_path, device='cpu', input_size=(256, 256)): - """Предсказание для одного изображения""" - # Загрузка изображения - image = Image.open(image_path).convert('RGB') - original_image = np.array(image) - original_size = image.size # (width, height) - - # Подготовка для модели (ресайз) - image_resized = image.resize(input_size) - image_array = np.array(image_resized, dtype=np.float32) / 255.0 - image_array = image_array.transpose(2, 0, 1) - image_tensor = torch.FloatTensor(image_array).unsqueeze(0).to(device) - - # Инференс - with torch.no_grad(): - output = model(image_tensor) - pred = torch.softmax(output, dim=1) - mask_resized = pred.argmax(dim=1).squeeze().cpu().numpy() - - # Ресайз маски обратно к оригинальному размеру - # Используем NEAREST интерполяцию для сохранения бинарности - mask_pil = Image.fromarray(mask_resized.astype(np.uint8)) - mask_pil = mask_pil.resize(original_size, resample=Image.NEAREST) - mask = np.array(mask_pil) - - return original_image, mask - - -def visualize_prediction(image, mask, quality_scores, save_path=None): - """Визуализация результатов""" - fig, axes = plt.subplots(1, 3, figsize=(15, 5)) - - # 1. Оригинальное изображение - axes[0].imshow(image) - axes[0].set_title('Оригинал') - axes[0].axis('off') - - # 2. Сегментация (маска наложена на оригинал) - axes[1].imshow(image) - axes[1].imshow(mask, cmap='jet', alpha=0.5) - axes[1].set_title('Сегментация') - axes[1].axis('off') - - # 3. Оценка качества - quality_text = f"Качество: {quality_scores['overall_quality']}\n" - quality_text += f"Уверенность: {quality_scores['confidence']:.2f}\n" - quality_text += f"Нарушений: {len(quality_scores['issues'])}\n\n" - - # Добавляем детали - if quality_scores['issues']: - quality_text += "Проблемы:\n" - for issue in quality_scores['issues']: - quality_text += f" • {issue['type']}: {issue['details'][:30]}...\n" - else: - quality_text += "✅ Нарушений не обнаружено" - - axes[2].text(0.05, 0.95, quality_text, fontsize=10, - verticalalignment='top', transform=axes[2].transAxes, - bbox=dict(boxstyle="round", facecolor="white", alpha=0.8)) - axes[2].set_title('Оценка качества') - axes[2].axis('off') - plt.rcParams['font.family'] = 'sans-serif' - plt.rcParams['font.sans-serif'] = ['Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', 'DejaVu Sans'] - plt.tight_layout() - - if save_path: - plt.savefig(save_path, dpi=150, bbox_inches='tight') - print(f"✅ Визуализация сохранена: {save_path}") - - plt.show() - - -def main(): - parser = argparse.ArgumentParser(description='Инференс модели сегментации') - parser.add_argument('--image', type=str, default="example.jpg", required=False, help='Путь к изображению') - parser.add_argument('--model', type=str, default='models/unet_cats_dogs.pth', - help='Путь к модели') - parser.add_argument('--device', type=str, default='cpu', - choices=['cpu', 'mps', 'cuda'], help='Устройство') - parser.add_argument('--save', type=str, default=None, help='Сохранить результат') - - args = parser.parse_args() - - # Проверка устройства - if args.device == 'mps' and not torch.backends.mps.is_available(): - print("⚠️ MPS не доступен, используем CPU") - args.device = 'cpu' - - print(f"🚀 Загрузка модели с {args.device}...") - model = load_model(args.model, device=args.device) - - print(f"📸 Анализ изображения: {args.image}") - image, mask = predict_single(model, args.image, device=args.device) - - print(f" Изображение: {image.shape}") - print(f" Маска: {mask.shape}") - - print("🔍 Оценка качества...") - scorer = QualityScorer() - quality_scores = scorer.evaluate(image=image, segmentation=mask) - - print(f"\n📊 Результаты:") - print(f" Качество: {quality_scores['overall_quality']}") - print(f" Уверенность: {quality_scores['confidence']:.2f}") - print(f" Количество проблем: {len(quality_scores['issues'])}") - - if quality_scores['issues']: - print("\n Проблемы:") - for issue in quality_scores['issues']: - print(f" • {issue['type']}: {issue['details'][:50]}...") - - # Визуализация - save_path = args.save or 'prediction_result.png' - visualize_prediction(image, mask, quality_scores, save_path) - - -if __name__ == "__main__": - main() diff --git a/requirements.txt b/requirements.txt index bbafbbf..31f914e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,47 +1,37 @@ -annotated-doc==0.0.5 -annotated-types==0.7.0 -anyio==4.12.1 -click==8.1.8 -contourpy==1.3.0 -cycler==0.12.1 -exceptiongroup==1.3.1 -fastapi==0.128.8 -filelock==3.19.1 -fonttools==4.60.2 -fsspec==2025.10.0 -h11==0.16.0 -idna==3.18 -importlib_resources==6.5.2 -Jinja2==3.1.6 -kiwisolver==1.4.7 -MarkupSafe==3.0.3 -matplotlib==3.9.4 -monai==1.5.2 -mpmath==1.3.0 -networkx==3.2.1 -nibabel==5.3.3 -numpy==2.0.2 -opencv-python-headless==5.0.0.93 -packaging==26.3 -pandas==2.3.3 -pillow==11.3.0 -pydantic==2.13.4 -pydantic_core==2.46.4 -pydicom==2.4.4 -pyparsing==3.3.2 -python-dateutil==2.9.0.post0 -python-multipart==0.0.20 -pytz==2026.3.post1 -scipy==1.13.1 -six==1.17.0 -starlette==0.49.3 -sympy==1.14.0 -torch==2.8.0 -torchvision==0.23.0 -TotalSegmentator==2.18.0 -tqdm==4.70.0 -typing-inspection==0.4.2 -typing_extensions==4.16.0 -tzdata==2026.3 -uvicorn==0.39.0 -zipp==3.23.1 +# 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 diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..4aac654 --- /dev/null +++ b/run.sh @@ -0,0 +1,134 @@ +#!/bin/bash +# DXA Quality Assessment - Main entry point +# Usage: bash run.sh [command] [args...] + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}=== DXA Quality Assessment ===${NC}" + +# Default values +COMMAND=${1:-help} +DATA_ROOT=${DATA_ROOT:-dataset_hack} +ANNOTATION_PATH=${ANNOTATION_PATH:-dataset_hack/НД_для_обучения/разметка.xlsx} +MODEL_PATH=${MODEL_PATH:-models/dxa_model.pth} +EPOCHS=${EPOCHS:-10} +BATCH_SIZE=${BATCH_SIZE:-16} + +case "$COMMAND" in + train) + echo -e "${YELLOW}Training model...${NC}" + + python3 -c " +import sys +sys.path.insert(0, '.') + +import torch +import numpy as np +from src.dxa.dataset import create_dataloaders +from src.dxa.model import create_model + +device = 'mps' if torch.backends.mps.is_available() else 'cpu' +print(f'Device: {device}') + +train_loader, val_loader = create_dataloaders( + data_root='${DATA_ROOT}', + annotation_path='${ANNOTATION_PATH}', + batch_size=${BATCH_SIZE}, + input_size=224, + num_workers=0 +) + +print(f'Train: {len(train_loader.dataset)}, Val: {len(val_loader.dataset)}') + +model = create_model(backbone='resnet18', pretrained=True, device=device) + +best_f1 = 0 +for epoch in range(${EPOCHS}): + train_loss, train_acc = model.train_epoch(train_loader) + val_loss, val_acc = model.validate(val_loader) + + # Compute F1 + model.model.eval() + preds, labels = [], [] + with torch.no_grad(): + for images, labs in val_loader: + outputs = model.model(images.to(device)) + preds.extend(outputs.argmax(dim=1).cpu().numpy()) + labels.extend(labs['label'].cpu().numpy()) + + preds, labels = np.array(preds), np.array(labels) + tp = ((preds == 1) & (labels == 1)).sum() + fp = ((preds == 1) & (labels == 0)).sum() + fn = ((preds == 0) & (labels == 1)).sum() + precision = tp/(tp+fp) if (tp+fp)>0 else 0 + recall = tp/(tp+fn) if (tp+fn)>0 else 0 + f1 = 2*precision*recall/(precision+recall) if (precision+recall)>0 else 0 + + print(f'Epoch {epoch+1}: Train={train_acc:.3f}, Val={val_acc:.3f}, F1={f1:.3f}') + + if f1 > best_f1: + best_f1 = f1 + model.save('${MODEL_PATH}') + +print(f'Best F1: {best_f1:.3f}') +" + echo -e "${GREEN}Training complete! Model saved to ${MODEL_PATH}${NC}" + ;; + + infer) + INPUT_PATH=${2:-dataset_hack/Для теста} + OUTPUT_PATH=${3:-results.xlsx} + + echo -e "${YELLOW}Running inference...${NC}" + echo "Input: ${INPUT_PATH}" + echo "Output: ${OUTPUT_PATH}" + + python3 -c " +import sys +sys.path.insert(0, '.') +from src.dxa.inference import process_dicom_files +import argparse + +process_dicom_files(argparse.Namespace( + input_path='${INPUT_PATH}', + output_path='${OUTPUT_PATH}', + model_path='${MODEL_PATH}', + backbone='resnet18', + input_size=224 +)) +" + echo -e "${GREEN}Inference complete!${NC}" + ;; + + serve) + echo -e "${YELLOW}Starting API server...${NC}" + python3 -m uvicorn src.main:app --host 0.0.0.0 --port 8000 + ;; + + help|*) + echo "Usage: $0 [command] [options]" + echo "" + echo "Commands:" + echo " train Train the model" + echo " infer Run inference" + echo " serve Start API server" + echo "" + echo "Environment variables:" + echo " DATA_ROOT Data directory (default: dataset_hack)" + echo " ANNOTATION_PATH Annotation Excel file" + echo " MODEL_PATH Model output path" + echo " EPOCHS Training epochs (default: 10)" + echo " BATCH_SIZE Batch size (default: 16)" + echo "" + echo "Examples:" + echo " $0 train" + echo " EPOCHS=50 $0 train" + echo " $0 infer dataset_hack/Для теста results.xlsx" + ;; +esac diff --git a/src/api/static/index.html b/src/api/static/index.html index dd2a24b..14adafa 100644 --- a/src/api/static/index.html +++ b/src/api/static/index.html @@ -3,873 +3,261 @@ - Bone Quality Assessment - Анализ качества костной ткани + DXA Quality Assessment + + + - - - 🦴 Bone Quality Assessment - Загрузите медицинское изображение для анализа качества костной ткани - - - - 📤 - Перетащите изображение сюда - или кликните для выбора файла - Поддерживаются: PNG, JPG, JPEG - - - - - - - - - - • - - - - - 🔍 Анализировать - - - - - - - 🧩 Визуализация сегментации - - - Прозрачность: - - 50% - - Скрыть маску - Показать оригинал - - - - - - - 🧊 Объектов: - - 📏 Размер: - - 🎯 Уверенность: - - - - - - - - Общее качество - - - - - Серьезность проблемы - - - - - Выявленные проблемы - - - - - - 📍 Позиционирование - - + + + + + + + - - 🔬 Артефакты - - + + DXA Quality Assessment + Оценка качества денситометрических исследований + + + + + + + + + + API Docs + + + + + + + + + + + + Проверка статуса сервиса... + + + + + + + + Загрузка DICOM файлов + + + + + + + + Перетащите DICOM файлы сюда или нажмите для выбора + + + Поддерживаются файлы .dcm + - - Уверенность модели - - - + + + + Загрузка и анализ... + 0% + + + + + + + + + + + + + + + + + + 0 + Всего файлов + + + + + + + + + + 0 + Качество OK + + + + + + + + + + 0 + Нарушения + + + + + + + + + + - + Точность + + - - Детальные метрики - + + + + + Скачать XLSX + + + + Очистить + - - - + - \ No newline at end of file +
Загрузите медицинское изображение для анализа качества костной ткани
или кликните для выбора файла
Поддерживаются: PNG, JPG, JPEG
Оценка качества денситометрических исследований
+ Перетащите DICOM файлы сюда или нажмите для выбора +
+ Поддерживаются файлы .dcm +
0
Всего файлов
Качество OK
Нарушения
-
Точность