Files
ai-med-edu/scripts/ttv_freq2.py
T

353 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# scripts/ttv_freq2.py
"""
Версия 6: Читает predefined_classes.txt для сохранения полной схемы классов.
Это позволяет использовать общую схему 12 классов для всех экспериментов,
даже если в конкретном датасете присутствуют не все классы.
"""
import os
import shutil
import random
import argparse
from pathlib import Path
try:
import yaml
except ImportError:
raise SystemExit("Установите PyYAML: pip install pyyaml")
def load_predefined_classes(path: Path) -> dict:
"""
Читает predefined_classes.txt и возвращает маппинг:
{имя_класса: (id_1based, yolo_index_0based)}
Формат файла (одна строка = один класс):
right_kidney
right_kidney_upper_segment
...
"""
class_map = {}
with open(path, 'r', encoding='utf-8') as f:
for idx, line in enumerate(f):
name = line.strip()
if name:
class_map[name] = {
'id_1based': idx + 1,
'yolo_idx': idx,
}
return class_map
def clean_caches(output_dir: Path):
"""Удаляет старые .cache файлы."""
for cache in output_dir.glob('*.cache'):
try:
cache.unlink()
print(f" Удалён кэш: {cache.name}")
except OSError:
pass
def remap_labels(src_dir: Path, dst_dir: Path, id_to_yolo_idx: dict):
"""
Копирует .txt файлы с перенумерацией классов из 1-based в 0-based.
"""
if dst_dir.exists():
shutil.rmtree(dst_dir)
dst_dir.mkdir(parents=True, exist_ok=True)
copied = 0
skipped = 0
for txt_file in src_dir.glob('*.txt'):
with open(txt_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
new_lines = []
for line in lines:
parts = line.strip().split()
if not parts:
continue
try:
old_class = int(parts[0])
if old_class in id_to_yolo_idx:
parts[0] = str(id_to_yolo_idx[old_class])
new_lines.append(' '.join(parts))
else:
skipped += 1
except ValueError:
continue
if new_lines:
with open(dst_dir / txt_file.name, 'w', encoding='utf-8') as f:
f.write('\n'.join(new_lines) + '\n')
copied += 1
return copied, skipped
def create_split(
images_dir: Path,
labels_dir: Path,
output_dir: Path,
task: str,
predefined_classes: dict,
train_ratio: float = 0.8,
val_ratio: float = 0.1,
seed: int = 42,
):
random.seed(seed)
images_dir = images_dir.resolve()
labels_dir = labels_dir.resolve()
output_dir = output_dir.resolve()
if not images_dir.exists():
raise FileNotFoundError(f"Папка с изображениями не найдена: {images_dir}")
if not labels_dir.exists():
raise FileNotFoundError(f"Папка с лейблами не найдена: {labels_dir}")
print("\nОчистка кэша...")
clean_caches(output_dir)
image_files = [f for f in os.listdir(images_dir)
if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
if not image_files:
raise ValueError(f"В {images_dir} не найдено изображений.")
print(f"\n{'='*60}")
print(f"Задача: {task.upper()}")
print(f"{'='*60}")
print(f"Изображения: {images_dir}")
print(f"Лейблы: {labels_dir}")
print(f"Найдено изображений: {len(image_files)}")
# Собираем статистику по классам
class_to_images = {}
classes_found = set()
for image_file in image_files:
label_file = os.path.splitext(image_file)[0] + '.txt'
label_path = labels_dir / label_file
if label_path.exists():
with open(label_path, 'r', encoding='utf-8') as f:
for line in f:
parts = line.strip().split()
if not parts:
continue
try:
class_label = int(parts[0])
classes_found.add(class_label)
class_to_images.setdefault(class_label, set()).add(image_file)
except ValueError:
continue
# Маппинг 1-based ID -> 0-based YOLO index
id_to_yolo_idx = {info['id_1based']: info['yolo_idx']
for info in predefined_classes.values()}
print(f"\n📋 Схема классов (из predefined_classes.txt):")
print(f" Всего классов: {len(predefined_classes)}")
present = []
absent = []
for name, info in predefined_classes.items():
if info['id_1based'] in classes_found:
present.append((name, info, len(class_to_images[info['id_1based']])))
else:
absent.append((name, info))
print(f"\n ✓ Присутствуют в датасете ({len(present)}):")
for name, info, count in present:
print(f" [{info['id_1based']:2d}] → YOLO idx {info['yolo_idx']:2d} : {name:<40s} ({count} изобр.)")
if absent:
print(f"\n ○ Отсутствуют ({len(absent)}):")
for name, info in absent:
print(f" [{info['id_1based']:2d}] → YOLO idx {info['yolo_idx']:2d} : {name}")
# Разбиение по наборам (стратифицированное)
sorted_classes = sorted(class_to_images, key=lambda k: len(class_to_images[k]))
train_files, val_files, test_files = set(), set(), set()
print(f"\n--- Распределение по наборам ---")
for class_label in sorted_classes:
images = set(class_to_images[class_label])
total = len(images)
train_count = max(1, int(total * train_ratio))
val_count = max(1, int(total * val_ratio))
if train_count + val_count > total:
val_count = max(0, total - train_count)
train_selected = set(random.sample(list(images), min(train_count, len(images))))
images -= train_selected
val_selected = set(random.sample(list(images), min(val_count, len(images))))
test_selected = images - val_selected
train_files |= train_selected
val_files |= val_selected
test_files |= test_selected
# Имя класса из predefined
class_name = next((n for n, info in predefined_classes.items()
if info['id_1based'] == class_label), f"class_{class_label}")
print(f" [{class_label:2d}] {class_name[:30]:<30s}: "
f"Train={len(train_selected):3d}, Val={len(val_selected):3d}, Test={len(test_selected):3d}")
for img in train_selected | val_selected | test_selected:
for cls_set in class_to_images.values():
cls_set.discard(img)
print(f"\nИТОГО: Train={len(train_files)}, Val={len(val_files)}, Test={len(test_files)}")
# Подготовка labels с перенумерацией
labels_dir_dst = output_dir / 'labels'
# Временная папка
raw_labels = output_dir / '_raw_labels'
if raw_labels.exists():
shutil.rmtree(raw_labels)
raw_labels.mkdir(parents=True, exist_ok=True)
print(f"\nКопирование лейблов...")
for img in train_files | val_files | test_files:
label_file = labels_dir / (Path(img).stem + '.txt')
if label_file.exists():
dst = raw_labels / (Path(img).stem + '.txt')
if not dst.exists():
shutil.copy2(label_file, dst)
print(f" Копирование с перенумерацией в YOLO 0-based формат:")
copied, skipped = remap_labels(raw_labels, labels_dir_dst, id_to_yolo_idx)
print(f" ✓ Обработано {copied} файлов, пропущено строк: {skipped}")
shutil.rmtree(raw_labels)
# Запись txt-файлов с абсолютными путями
def write_split(file_path: Path, file_set: set):
with open(file_path, 'w', encoding='utf-8') as f:
for img in sorted(file_set):
abs_path = str(images_dir / img).replace('\\', '/')
f.write(abs_path + '\n')
train_txt = output_dir / f'train_{task}.txt'
val_txt = output_dir / f'val_{task}.txt'
test_txt = output_dir / f'test_{task}.txt'
write_split(train_txt, train_files)
write_split(val_txt, val_files)
write_split(test_txt, test_files)
# data.yaml — ВСЕГДА с полной схемой из predefined_classes.txt
nc = len(predefined_classes)
class_names = list(predefined_classes.keys())
data_yaml = {
'path': str(output_dir).replace('\\', '/'),
'train': str(train_txt).replace('\\', '/'),
'val': str(val_txt).replace('\\', '/'),
'test': str(test_txt).replace('\\', '/'),
'nc': nc,
'names': class_names,
}
yaml_name = f'data_{task}.yaml'
yaml_path = output_dir / yaml_name
with open(yaml_path, 'w', encoding='utf-8') as f:
yaml.safe_dump(data_yaml, f, sort_keys=False, allow_unicode=True)
print(f"\n✓ Создан {yaml_name}: {yaml_path}")
print(f"✓ nc={nc}, классов: {nc}")
print(f"✓ Классы (0..{nc-1}): {class_names}")
return yaml_path
def main():
parser = argparse.ArgumentParser(
description='Создание train/val/test сплита с учётом predefined_classes.txt'
)
parser.add_argument('--task', type=str, required=True,
choices=['detect', 'segment', 'both'])
parser.add_argument('--images-dir', type=str, required=True)
parser.add_argument('--labels-dir', type=str, required=True)
parser.add_argument('--polygons-dir', type=str, default=None)
parser.add_argument('--output-dir', type=str, required=True)
parser.add_argument('--predefined-classes', type=str, default='predefined_classes.txt',
help='Путь к файлу с полным списком классов (по умолчанию: predefined_classes.txt)')
parser.add_argument('--train-ratio', type=float, default=0.8)
parser.add_argument('--val-ratio', type=float, default=0.1)
parser.add_argument('--seed', type=int, default=42)
args = parser.parse_args()
# Загружаем полную схему классов
predefined_path = Path(args.predefined_classes).resolve()
if not predefined_path.exists():
raise FileNotFoundError(f"Файл классов не найден: {predefined_path}\n"
f"Создайте его (по одному классу на строку) или укажите путь через --predefined-classes")
predefined_classes = load_predefined_classes(predefined_path)
print(f"📖 Загружена схема классов: {predefined_path}")
print(f" Всего классов: {len(predefined_classes)}")
if args.task == 'both':
if not args.polygons_dir:
raise ValueError("Для task=both необходимо указать --polygons-dir")
create_split(
images_dir=Path(args.images_dir),
labels_dir=Path(args.labels_dir),
output_dir=Path(args.output_dir),
task='detect',
predefined_classes=predefined_classes,
train_ratio=args.train_ratio,
val_ratio=args.val_ratio,
seed=args.seed,
)
create_split(
images_dir=Path(args.images_dir),
labels_dir=Path(args.polygons_dir),
output_dir=Path(args.output_dir),
task='segment',
predefined_classes=predefined_classes,
train_ratio=args.train_ratio,
val_ratio=args.val_ratio,
seed=args.seed,
)
print(f"\n{'='*60}")
print("✅ ОБА SPLITS СОЗДАНЫ!")
print(f"{'='*60}")
else:
create_split(
images_dir=Path(args.images_dir),
labels_dir=Path(args.labels_dir),
output_dir=Path(args.output_dir),
task=args.task,
predefined_classes=predefined_classes,
train_ratio=args.train_ratio,
val_ratio=args.val_ratio,
seed=args.seed,
)
print(f"\n{'='*60}")
print("✅ SPLT СОЗДАН!")
print(f"{'='*60}")
yaml_name = f'data_{args.task}.yaml'
if args.task == 'detect':
print(f"Запуск: python scripts/train_yolo.py --data {Path(args.output_dir) / yaml_name} --workers 0")
else:
print(f"Запуск: python scripts/train_yolo_seg.py --data {Path(args.output_dir) / yaml_name} --workers 0")
if __name__ == '__main__':
main()