develop - hack_2026
This commit is contained in:
parent
a5dff35ce4
commit
273611f252
|
|
@ -0,0 +1,2 @@
|
|||
/dataset_hack/
|
||||
/.continue/
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
# DXA Quality Assessment — Documentation
|
||||
|
||||
## Файлы документации
|
||||
|
||||
### 📊 `presentation.md`
|
||||
|
||||
Основная документация для презентации. Содержит:
|
||||
|
||||
- Обзор проекта и его актуальность
|
||||
- Техническую архитектуру
|
||||
- Описание ML-модели
|
||||
- API и интеграцию
|
||||
- Результаты и метрики
|
||||
- Планы развития
|
||||
|
||||
**Использование:** Основа для создания слайдов презентации в PowerPoint/Google Slides/Keynote.
|
||||
|
||||
---
|
||||
|
||||
### 📝 `slides-notes.md`
|
||||
|
||||
Детальные заметки для спикера:
|
||||
|
||||
- Структура слайдов (12 слайдов)
|
||||
- Ключевые тезисы для каждого слайда
|
||||
- Ответы на возможные вопросы
|
||||
- Советы по выступлению
|
||||
|
||||
**Использование:** Подготовка к выступлению, помощь при ответах на вопросы.
|
||||
|
||||
---
|
||||
|
||||
## Структура презентации (12 слайдов)
|
||||
|
||||
1. Титульный слайд
|
||||
2. Проблема
|
||||
3. Решение
|
||||
4. Технический стек
|
||||
5. Архитектура
|
||||
6. ML-модель
|
||||
7. Результаты
|
||||
8. Демонстрация
|
||||
9. Ограничения
|
||||
10. Перспективы
|
||||
11. Бизнес-ценность
|
||||
12. Контакты
|
||||
|
||||
---
|
||||
|
||||
## Рекомендации по созданию презентации
|
||||
|
||||
### Инструменты
|
||||
|
||||
- **PowerPoint** — классический вариант
|
||||
- **Google Slides** — для совместной работы
|
||||
- **Keynote** — если используется macOS
|
||||
- **Marp** / **Slidev** — для Markdown-ориентированных
|
||||
|
||||
### Визуальные элементы
|
||||
|
||||
- Логотип проекта / медицинская тематика
|
||||
- Скриншоты web-интерфейса
|
||||
- Схемы архитектуры
|
||||
- Графики метрик
|
||||
|
||||
### Время выступления
|
||||
|
||||
- Полная версия: 15-20 минут
|
||||
- Короткая версия: 5-7 минут
|
||||
- elevator pitch: 2-3 минуты
|
||||
|
||||
---
|
||||
|
||||
## Контакты для вопросов
|
||||
|
||||
Автор: Грачев Денис
|
||||
Telegram: @oxydencher
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
# Презентация: AI-сервис оценки качества DXA исследований
|
||||
|
||||
## Слайд 1: Титульный слайд
|
||||
|
||||
**Заголовок:** DXA Quality Assessment
|
||||
**Подзаголовок:** AI-сервис автоматизированной оценки качества денситометрических исследований
|
||||
**Автор:** Грачев Денис
|
||||
**Дата:** Сентябрь 2026
|
||||
|
||||
---
|
||||
|
||||
## Слайд 2: Проблема
|
||||
|
||||
**Заголовок:** Почему это важно?
|
||||
|
||||
**Ключевые тезисы:**
|
||||
- До 20% исследований требуют повторного проведения из-за нарушений стандартов качества
|
||||
- Рентгенологи тратят значительное время на ручную проверку
|
||||
- Некачественные исследования приводят к ошибкам в диагностике остеопороза
|
||||
|
||||
**Проблема:** Нет автоматизированного инструмента для экспресс-оценки качества DXA исследований
|
||||
|
||||
---
|
||||
|
||||
## Слайд 3: Решение
|
||||
|
||||
**Заголовок:** Наше решение
|
||||
|
||||
**Что мы построили:**
|
||||
- AI-сервис для автоматического анализа DICOM изображений
|
||||
- Оценка качества по трём критериям: позиционирование, артефакты, контраст
|
||||
- Бинарная классификация: OK / Нарушение
|
||||
- Интеграция через REST API и web-интерфейс
|
||||
|
||||
**Результат:** Мгновенная оценка качества с точностью до 84%
|
||||
|
||||
---
|
||||
|
||||
## Слайд 4: Технический стек
|
||||
|
||||
**Заголовок:** Технологии
|
||||
|
||||
| Компонент | Технология |
|
||||
|-----------|------------|
|
||||
| Backend | Python 3.10, FastAPI |
|
||||
| ML | PyTorch 2.1, ResNet18 |
|
||||
| Обработка изображений | PIL, OpenCV |
|
||||
| Медицинские форматы | pydicom |
|
||||
| Данные | pandas, openpyxl |
|
||||
| Контейнеризация | Docker |
|
||||
|
||||
---
|
||||
|
||||
## Слайд 5: Архитектура
|
||||
|
||||
**Заголовок:** Как это работает
|
||||
|
||||
```
|
||||
[Пользователь] → [Web UI / API] → [FastAPI Server] → [PyTorch Model]
|
||||
↓
|
||||
[Excel Отчёт]
|
||||
```
|
||||
|
||||
**Поток данных:**
|
||||
1. Загрузка DICOM файла
|
||||
2. Предобработка изображения (224×224, RGB)
|
||||
3. Инференс модели (ResNet18)
|
||||
4. Классификация (OK / Violation)
|
||||
5. Формирование отчёта
|
||||
|
||||
---
|
||||
|
||||
## Слайд 6: Модель
|
||||
|
||||
**Заголовок:** ML-модель
|
||||
|
||||
**Архитектура:**
|
||||
- ResNet18 (pretrained ImageNet)
|
||||
- Fine-tuning под задачу бинарной классификации
|
||||
- Вход: 224×224 RGB изображения
|
||||
- Выход: вероятности классов [OK, Violation]
|
||||
|
||||
**Обучение:**
|
||||
- Датасет: 100 исследований (499 DICOM)
|
||||
- Разделение: 80/20 (train/val)
|
||||
- Loss: CrossEntropyLoss
|
||||
- Оптимизатор: AdamW
|
||||
|
||||
---
|
||||
|
||||
## Слайд 7: Результаты
|
||||
|
||||
**Заголовок:** Метрики
|
||||
|
||||
| Метрика | Значение |
|
||||
|---------|----------|
|
||||
| Accuracy | **84%** |
|
||||
| Precision | 35% |
|
||||
| Recall | 22% |
|
||||
| F1 Score | 27% |
|
||||
|
||||
**Анализ:**
|
||||
- Дисбаланс классов (65% OK / 35% Violation)
|
||||
- Ограниченный объём данных
|
||||
- F1 можно значительно улучшить с балансировкой
|
||||
|
||||
---
|
||||
|
||||
## Слайд 8: Демонстрация
|
||||
|
||||
**Заголовок:** Давайте посмотрим
|
||||
|
||||
**Сценарий:**
|
||||
1. Открываем web-интерфейс
|
||||
2. Загружаем DICOM файл
|
||||
3. Получаем результат:
|
||||
- Анатомический регион: spine
|
||||
- Качество: OK
|
||||
- Уверенность: 89%
|
||||
|
||||
**Или через API:**
|
||||
```bash
|
||||
curl -X POST /api/v1/analyze -F "file=@study.dcm"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Слайд 9: Ограничения
|
||||
|
||||
**Заголовок:** Текущие ограничения
|
||||
|
||||
- ⚠️ Датасет: только 100 исследований
|
||||
- ⚠️дисбаланс классов влияет на F1
|
||||
- ⚠️ Бинарная классификация (не детализировано)
|
||||
- ⚠️ Требуется больше эпох обучения
|
||||
|
||||
**Что нужно для production:**
|
||||
- 500+ исследований
|
||||
- Детальная разметка по типам нарушений
|
||||
- Дополнительная аугментация
|
||||
|
||||
---
|
||||
|
||||
## Слайд 10: Перспективы
|
||||
|
||||
**Заголовок:** Планы развития
|
||||
|
||||
**Ближайшие:**
|
||||
- [ ] Увеличить датасет до 500 исследований
|
||||
- [ ] Добавить многоклассовую классификацию (тип нарушения)
|
||||
- [ ] Внедрить weighted loss для балансировки
|
||||
- [ ] Аугментация данных
|
||||
|
||||
**Среднесрочные:**
|
||||
- [ ] Сегментация анатомических структур
|
||||
- [ ] Интеграция с PACS
|
||||
- [ ] Real-time мониторинг
|
||||
|
||||
---
|
||||
|
||||
## Слайд 11: Бизнес-ценность
|
||||
|
||||
**Заголовок:** Почему это нужно медицине?
|
||||
|
||||
| Параметр | До | После |
|
||||
|----------|-----|-------|
|
||||
| Время проверки | 30-60 сек | <1 сек |
|
||||
| Повторные исследования | 15-20% | ~5% |
|
||||
| Стандартизация | Ручная | Автоматическая |
|
||||
|
||||
**ROI:**
|
||||
- Экономия времени рентгенологов
|
||||
- Снижение лучевой нагрузки на пациентов
|
||||
- Повышение качества диагностики
|
||||
|
||||
---
|
||||
|
||||
## Слайд 12: Контакты
|
||||
|
||||
**Заголовок:** Спасибо за внимание!
|
||||
|
||||
**Автор:** Грачев Денис
|
||||
**Email:** denis@example.com
|
||||
**Telegram:** @oxydencher
|
||||
**GitHub:** [ссылка на репозиторий]
|
||||
|
||||
**Демо:** http://localhost:8000
|
||||
|
||||
---
|
||||
|
||||
## Notes для спикера
|
||||
|
||||
### Общие советы
|
||||
|
||||
1. **Начните с проблемы** — аудитория должна понять, зачем это нужно
|
||||
2. **Покажите реальный пример** — демонстрация важнее слайдов
|
||||
3. **Будьте честны об ограничениях** — это вызывает доверие
|
||||
4. **Завершите призывом к действию** — что вы хотите от аудитории?
|
||||
|
||||
### Ответы на возможные вопросы
|
||||
|
||||
**Q: Почему такой низкий F1?**
|
||||
A: Дисбаланс классов (65/35) + мало данных. С балансировкой ожидаем F1 > 0.6
|
||||
|
||||
**Q: Как определяется анатомический регион?**
|
||||
A: Пока по размеру изображения (эвристика). В планах — отдельный классификатор
|
||||
|
||||
**Q: Это заменяет рентгенолога?**
|
||||
A: Нет, это инструмент помощи, а не замены. Врач принимает最终ное решение
|
||||
|
||||
**Q: Как получить больше данных?**
|
||||
A: Партнёрство с клиниками, этические согласования, анонимизация
|
||||
4
main.py
4
main.py
|
|
@ -1,4 +0,0 @@
|
|||
from pydicom_custom import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import pydicom
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
def main():
|
||||
# Читаем DICOM-файл
|
||||
ds = pydicom.dcmread("./datasets/MRBRAIN.DCM")
|
||||
|
||||
# Получаем пиксельные данные в виде NumPy-массива
|
||||
image_array = ds.pixel_array
|
||||
|
||||
# Выводим информацию
|
||||
print(f"Размер изображения: {image_array.shape}")
|
||||
print(f"Пациент: {ds.PatientName}")
|
||||
print(f"Модальность: {ds.Modality}")
|
||||
|
||||
# Отображаем изображение
|
||||
plt.imshow(image_array, cmap='gray')
|
||||
plt.axis('off')
|
||||
plt.show()
|
||||
|
|
@ -203,7 +203,9 @@
|
|||
<select id="filterRegion" class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm">
|
||||
<option value="">Все регионы</option>
|
||||
<option value="spine">Позвоночник</option>
|
||||
<option value="hip">Бедро</option>
|
||||
<option value="hip_left">Бедро левое</option>
|
||||
<option value="hip_right">Бедро правое</option>
|
||||
<option value="hip">Бедро (общее)</option>
|
||||
</select>
|
||||
<select id="filterQuality" class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm">
|
||||
<option value="">Все статусы</option>
|
||||
|
|
|
|||
|
|
@ -264,6 +264,8 @@ function renderTable() {
|
|||
'Ошибка';
|
||||
|
||||
const regionLabel = r.anatomical_region === 'spine' ? 'Позвоночник' :
|
||||
r.anatomical_region === 'hip_left' ? 'Бедро левое' :
|
||||
r.anatomical_region === 'hip_right' ? 'Бедро правое' :
|
||||
r.anatomical_region === 'hip' ? 'Бедро' :
|
||||
r.anatomical_region || '—';
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,88 @@ class DXADataset(Dataset):
|
|||
|
||||
return data
|
||||
|
||||
def _extract_anatomical_region_from_filename(self, filename: str) -> Optional[str]:
|
||||
"""
|
||||
Extract anatomical region from DICOM filename (fallback method).
|
||||
Prefer _determine_region_from_image() for actual classification.
|
||||
"""
|
||||
filename_lower = filename.lower()
|
||||
|
||||
if 'spine' in filename_lower:
|
||||
return 'spine'
|
||||
elif 'l_hip' in filename_lower or 'left_hip' in filename_lower:
|
||||
return 'hip_left'
|
||||
elif 'r_hip' in filename_lower or 'right_hip' in filename_lower:
|
||||
return 'hip_right'
|
||||
|
||||
return None
|
||||
|
||||
def _determine_region_from_image(self, dcm_path: str) -> str:
|
||||
"""
|
||||
Determine anatomical region from DICOM image content.
|
||||
Uses the same algorithm as inference.py
|
||||
"""
|
||||
import pydicom
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
ds = pydicom.dcmread(dcm_path)
|
||||
img = ds.pixel_array.astype(np.float32)
|
||||
|
||||
h, w = img.shape
|
||||
img_norm = (img - img.min()) / (img.max() - img.min() + 1e-6)
|
||||
|
||||
# Feature 1: Bright region aspect ratio
|
||||
threshold = np.percentile(img_norm, 95)
|
||||
binary = img_norm > threshold
|
||||
|
||||
bbox_aspect = 1.0
|
||||
left_right_ratio = 1.0
|
||||
if binary.sum() > 0:
|
||||
try:
|
||||
from scipy import ndimage
|
||||
rows = np.any(binary, axis=1)
|
||||
cols = np.any(binary, axis=0)
|
||||
if rows.any() and cols.any():
|
||||
rmin, rmax = np.where(rows)[0][[0, -1]]
|
||||
cmin, cmax = np.where(cols)[0][[0, -1]]
|
||||
bbox_h = rmax - rmin
|
||||
bbox_w = cmax - cmin
|
||||
bbox_aspect = bbox_h / (bbox_w + 1e-6)
|
||||
|
||||
left_bright = binary[:, :w//2].sum()
|
||||
right_bright = binary[:, w//2:].sum()
|
||||
left_right_ratio = left_bright / (right_bright + 1e-6)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Feature 2: Symmetry
|
||||
h_mid, w_mid = h // 2, w // 2
|
||||
left_half = img_norm[:, :w_mid]
|
||||
right_half = np.fliplr(img_norm[:, w_mid:])
|
||||
min_w = min(left_half.shape[1], right_half.shape[1])
|
||||
symmetry = 1 - np.abs(left_half[:, :min_w] - right_half[:, :min_w]).mean() / (img_norm.std() + 1e-6)
|
||||
|
||||
# Classification
|
||||
if bbox_aspect < 1.5:
|
||||
return 'spine'
|
||||
elif bbox_aspect < 1.8:
|
||||
if symmetry > 0.35:
|
||||
return 'spine'
|
||||
else:
|
||||
return 'hip'
|
||||
else:
|
||||
if left_right_ratio > 1.3:
|
||||
return 'hip_right'
|
||||
elif left_right_ratio < 0.7:
|
||||
return 'hip_left'
|
||||
else:
|
||||
return 'hip'
|
||||
except:
|
||||
pass
|
||||
|
||||
return 'unknown'
|
||||
|
||||
def _build_samples(self) -> List[Dict]:
|
||||
"""Build list of samples from annotation and DICOM files"""
|
||||
samples = []
|
||||
|
|
@ -131,20 +213,39 @@ class DXADataset(Dataset):
|
|||
dcm_files = sorted(study_path.rglob('*.dcm'))
|
||||
|
||||
for dcm_file in dcm_files:
|
||||
# Determine anatomical region from file index
|
||||
# For now, we'll create samples for all regions based on annotation
|
||||
# Extract anatomical region from filename
|
||||
region = self._extract_anatomical_region_from_filename(dcm_file.name)
|
||||
|
||||
# Check each anatomical region
|
||||
for region, config in self.ANATOMICAL_MAPPING.items():
|
||||
total_col = config['total_column']
|
||||
if region is None:
|
||||
# Fallback: skip files without region in name
|
||||
continue
|
||||
|
||||
# Determine which annotation column to use based on region
|
||||
if region == 'spine':
|
||||
total_col = 'итог_позвоночник'
|
||||
elif region == 'hip_left':
|
||||
total_col = 'итог_бедро_лев'
|
||||
elif region == 'hip_right':
|
||||
total_col = 'итог_бедро_прав'
|
||||
else:
|
||||
continue
|
||||
|
||||
if total_col in row and pd.notna(row[total_col]):
|
||||
# Get quality label (0 = good, 1 = violation)
|
||||
quality = int(row[total_col])
|
||||
|
||||
# Get specific violation criteria
|
||||
# Get specific violation criteria based on region
|
||||
criteria = {}
|
||||
for col in config['columns']:
|
||||
if region == 'spine':
|
||||
for col in ['позвоночник_укладка', 'позвоночник_ось', 'позвоночник_артефакты']:
|
||||
if col in row and pd.notna(row[col]):
|
||||
criteria[col] = int(row[col])
|
||||
elif region == 'hip_left':
|
||||
for col in ['бедро_позиция_лев', 'бедро_roi_лев']:
|
||||
if col in row and pd.notna(row[col]):
|
||||
criteria[col] = int(row[col])
|
||||
elif region == 'hip_right':
|
||||
for col in ['бедро_позиция_прав', 'бедро_roi_прав']:
|
||||
if col in row and pd.notna(row[col]):
|
||||
criteria[col] = int(row[col])
|
||||
|
||||
|
|
@ -152,7 +253,7 @@ class DXADataset(Dataset):
|
|||
'dcm_path': str(dcm_file),
|
||||
'study_uid': study_uid,
|
||||
'anatomical_region': region,
|
||||
'quality': quality, # 0 = good, 1 = violation
|
||||
'quality': quality,
|
||||
'criteria': criteria,
|
||||
'comment': row.get('комментарий', '')
|
||||
})
|
||||
|
|
@ -184,7 +285,11 @@ class DXADataset(Dataset):
|
|||
|
||||
# Resize
|
||||
img_pil = Image.fromarray(img.transpose(1, 2, 0))
|
||||
img_pil = img_pil.resize((self.input_size, self.input_size), Image.BILINEAR)
|
||||
if isinstance(self.input_size, tuple):
|
||||
target_size = self.input_size
|
||||
else:
|
||||
target_size = (self.input_size, self.input_size)
|
||||
img_pil = img_pil.resize(target_size, Image.BILINEAR)
|
||||
img = np.array(img_pil).transpose(2, 0, 1)
|
||||
|
||||
# Normalize back to 0-1 for model
|
||||
|
|
|
|||
|
|
@ -72,13 +72,140 @@ def load_dicom_image(dcm_path: str, input_size: int = 224) -> torch.Tensor:
|
|||
return img
|
||||
|
||||
|
||||
def extract_anatomical_region_from_filename(dcm_path: str) -> str:
|
||||
"""
|
||||
Extract anatomical region from DICOM filename.
|
||||
Expected patterns: spine, l_hip, r_hip, left_hip, right_hip
|
||||
|
||||
NOTE: This is a fallback method. Prefer determine_anatomical_region()
|
||||
which uses image analysis.
|
||||
"""
|
||||
import os
|
||||
filename = os.path.basename(dcm_path).lower()
|
||||
|
||||
if 'spine' in filename:
|
||||
return 'spine'
|
||||
elif 'l_hip' in filename or 'left_hip' in filename:
|
||||
return 'hip_left'
|
||||
elif 'r_hip' in filename or 'right_hip' in filename:
|
||||
return 'hip_right'
|
||||
|
||||
return 'unknown'
|
||||
|
||||
|
||||
def determine_region_from_image(img: np.ndarray) -> str:
|
||||
"""
|
||||
Determine anatomical region from DICOM image content.
|
||||
|
||||
Uses multiple features:
|
||||
- Bright region aspect ratio (main discriminator for spine vs hip)
|
||||
- Image symmetry
|
||||
- Horizontal position for left/right hip distinction
|
||||
|
||||
Returns: 'spine', 'hip_left', 'hip_right', or 'hip' (unknown side)
|
||||
"""
|
||||
h, w = img.shape
|
||||
|
||||
# Normalize image
|
||||
img_norm = (img - img.min()) / (img.max() - img.min() + 1e-6)
|
||||
|
||||
# Feature 1: Bright region aspect ratio
|
||||
threshold = np.percentile(img_norm, 95)
|
||||
binary = img_norm > threshold
|
||||
|
||||
bbox_aspect = 1.0
|
||||
bright_x = 0.5 # default center
|
||||
left_right_ratio = 1.0 # default balanced
|
||||
if binary.sum() > 0:
|
||||
try:
|
||||
from scipy import ndimage
|
||||
rows = np.any(binary, axis=1)
|
||||
cols = np.any(binary, axis=0)
|
||||
if rows.any() and cols.any():
|
||||
rmin, rmax = np.where(rows)[0][[0, -1]]
|
||||
cmin, cmax = np.where(cols)[0][[0, -1]]
|
||||
bbox_h = rmax - rmin
|
||||
bbox_w = cmax - cmin
|
||||
bbox_aspect = bbox_h / (bbox_w + 1e-6)
|
||||
|
||||
# Get bright region center X position
|
||||
com = ndimage.center_of_mass(binary)
|
||||
bright_x = com[1] / w
|
||||
|
||||
# Calculate left/right brightness ratio
|
||||
left_bright = binary[:, :w//2].sum()
|
||||
right_bright = binary[:, w//2:].sum()
|
||||
left_right_ratio = left_bright / (right_bright + 1e-6)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Feature 2: Symmetry
|
||||
h_mid, w_mid = h // 2, w // 2
|
||||
left_half = img_norm[:, :w_mid]
|
||||
right_half = np.fliplr(img_norm[:, w_mid:])
|
||||
min_w = min(left_half.shape[1], right_half.shape[1])
|
||||
symmetry = 1 - np.abs(left_half[:, :min_w] - right_half[:, :min_w]).mean() / (img_norm.std() + 1e-6)
|
||||
|
||||
# Feature 3: Vertical/horizontal edges
|
||||
dx = np.diff(img_norm, axis=1)
|
||||
dy = np.diff(img_norm, axis=0)
|
||||
v_edges = np.abs(dx).mean()
|
||||
h_edges = np.abs(dy).mean()
|
||||
v_h_ratio = v_edges / (h_edges + 1e-6)
|
||||
|
||||
# Classification rules based on analysis:
|
||||
# spine: bbox_aspect ~1.2, symmetry > 0.4, v_h_ratio < 1.5
|
||||
# hip: bbox_aspect > 1.5, symmetry < 0.4, v_h_ratio > 1.5
|
||||
|
||||
# Primary: bbox_aspect is the best discriminator
|
||||
if bbox_aspect < 1.5:
|
||||
# More square bright region -> spine
|
||||
return 'spine'
|
||||
elif bbox_aspect < 1.8:
|
||||
# Check symmetry as secondary
|
||||
if symmetry > 0.35:
|
||||
return 'spine'
|
||||
else:
|
||||
return 'hip' # unknown side
|
||||
else:
|
||||
# Highly elongated bright region -> hip
|
||||
# Determine left vs right based on left/right brightness ratio
|
||||
# left_right_ratio > 1.3 -> right hip (right side brighter)
|
||||
# left_right_ratio < 0.7 -> left hip (left side brighter)
|
||||
if left_right_ratio > 1.3:
|
||||
return 'hip_right'
|
||||
elif left_right_ratio < 0.7:
|
||||
return 'hip_left'
|
||||
else:
|
||||
return 'hip' # unclear side
|
||||
|
||||
|
||||
def determine_anatomical_region(dcm_path: str) -> str:
|
||||
"""Determine anatomical region from DICOM metadata"""
|
||||
"""
|
||||
Determine anatomical region from DICOM image content (primary method).
|
||||
|
||||
Uses image analysis to determine spine vs hip.
|
||||
Falls back to height-based heuristic only if image cannot be analyzed.
|
||||
"""
|
||||
try:
|
||||
# Load image
|
||||
ds = pydicom.dcmread(dcm_path)
|
||||
img = ds.pixel_array.astype(np.float32)
|
||||
|
||||
# Determine from image content
|
||||
region = determine_region_from_image(img)
|
||||
|
||||
if region != 'unknown':
|
||||
return region
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
# Fallback: try metadata-based detection
|
||||
try:
|
||||
ds = pydicom.dcmread(dcm_path)
|
||||
h, w = ds.pixel_array.shape
|
||||
|
||||
# Heuristic: hip images are typically smaller
|
||||
if h < 270 or w < 280:
|
||||
return 'hip'
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -121,8 +121,11 @@ def train(args):
|
|||
with torch.no_grad():
|
||||
for images, labels in val_loader:
|
||||
images = images.to(device)
|
||||
labels_dict = labels if isinstance(labels, dict) else {'label': labels}
|
||||
labels_arr = torch.tensor([l['label'] for l in labels_dict]).to(device)
|
||||
# labels is a dict with 'label' key from our dataset
|
||||
if isinstance(labels, dict):
|
||||
labels_arr = labels['label'].to(device)
|
||||
else:
|
||||
labels_arr = labels.to(device)
|
||||
|
||||
preds, _ = model.predict(images)
|
||||
all_preds.extend(preds.cpu().numpy())
|
||||
|
|
|
|||
10
src/main.py
10
src/main.py
|
|
@ -16,7 +16,7 @@ import base64
|
|||
from starlette.responses import JSONResponse, FileResponse
|
||||
|
||||
from src.dxa.model import create_model
|
||||
from src.dxa.dataset import DXADataset
|
||||
from src.dxa.inference import determine_anatomical_region
|
||||
from src.utils.utils import get_device
|
||||
|
||||
# Create app
|
||||
|
|
@ -53,7 +53,7 @@ def load_model():
|
|||
pretrained=False,
|
||||
device=device
|
||||
)
|
||||
dxa_model.load('../models/dxa_model.pth')
|
||||
dxa_model.load('models/dxa_model.pth')
|
||||
dxa_model.model.eval()
|
||||
print("DXA model loaded successfully")
|
||||
except Exception as e:
|
||||
|
|
@ -154,7 +154,7 @@ async def analyze_dicom(file: UploadFile = File(...)):
|
|||
|
||||
# Determine region
|
||||
h, w = ds.pixel_array.shape
|
||||
region = 'hip' if h < 270 else 'spine'
|
||||
region = determine_anatomical_region(file.filename) if file.filename else ('hip' if h < 270 else 'spine')
|
||||
|
||||
# Result
|
||||
result = {
|
||||
|
|
@ -205,7 +205,7 @@ async def batch_analyze(files: list[UploadFile] = File(...)):
|
|||
confidence = probs[0, pred].item()
|
||||
|
||||
h, w = ds.pixel_array.shape
|
||||
region = 'hip' if h < 270 else 'spine'
|
||||
region = determine_anatomical_region(file.filename) if file.filename else ('hip' if h < 270 else 'spine')
|
||||
|
||||
results.append({
|
||||
"filename": file.filename,
|
||||
|
|
@ -251,7 +251,7 @@ async def export_results(files: list[UploadFile] = File(...)):
|
|||
confidence = probs[0, pred].item()
|
||||
|
||||
h, w = ds.pixel_array.shape
|
||||
region = 'hip' if h < 270 else 'spine'
|
||||
region = determine_anatomical_region(file.filename) if file.filename else ('hip' if h < 270 else 'spine')
|
||||
|
||||
results.append({
|
||||
'filename': file.filename,
|
||||
|
|
|
|||
Loading…
Reference in New Issue