This commit is contained in:
pikaliov
2026-07-07 18:47:38 +03:00
parent 3e95f4f618
commit 05355c6a7b
9 changed files with 599 additions and 337 deletions

125
check_coverage.py Normal file
View File

@@ -0,0 +1,125 @@
"""Сравнение ПОКРЫТИЯ captions двух наборов (v1 vs v2) — отдельно от обучения.
Отвечает на один вопрос: одинаковый ли набор картинок покрыт текстом в v1 и v2?
Если да — сравнение R@1 между версиями чистое. Если нет — разница R@1 может
объясняться покрытием, а не качеством текста (см. протокол §5.4, §0.4).
Использует ТЕ ЖЕ функции чтения, что и обучение (load_text_descriptions,
combine_text_levels), поэтому «есть текст» здесь означает ровно то же, что
видит модель: непустой текст для выбранных text_levels, а не заглушку
"No description available.".
Пример:
python check_coverage.py \
--data_root /media/servml/SSD_2_2TB/datasets/cvgl_datasets/GTA-UAV-LR \
--captions_v1 ".../GTA-UAV-LR-captions_ v1" \
--captions_v2 ".../GTA-UAV-LR-captions_v2" \
--text_levels level1 level2
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from src.data.gta_uav import combine_text_levels, load_text_descriptions
from src.data.gta_uav_eval import collect_gallery_names
def _has_real_text(
name: str,
descriptions: dict[str, dict],
text_levels: list[str],
) -> bool:
"""True, если для картинки есть непустой текст на выбранных уровнях.
Повторяет семантику обучения: пустой результат combine_text_levels →
модель получит заглушку, т.е. текста фактически нет.
"""
desc = descriptions.get(name)
if not desc:
return False
combined = combine_text_levels(desc, text_levels)
return combined.strip() != "" and combined != "No description available."
def _covered_set(
names: list[str],
descriptions: dict[str, dict],
text_levels: list[str],
) -> set[str]:
"""Подмножество имён, у которых есть реальный текст."""
return {n for n in names if _has_real_text(n, descriptions, text_levels)}
def _report_side(
title: str,
names: list[str],
desc_v1: dict[str, dict],
desc_v2: dict[str, dict],
text_levels: list[str],
) -> None:
"""Напечатать покрытие v1/v2 и пересечение для одной стороны."""
total = len(names)
cov_v1 = _covered_set(names, desc_v1, text_levels)
cov_v2 = _covered_set(names, desc_v2, text_levels)
both = cov_v1 & cov_v2
only_v1 = cov_v1 - cov_v2
only_v2 = cov_v2 - cov_v1
def pct(n: int) -> str:
return f"{n:>6d} / {total} ({100.0 * n / max(total, 1):5.1f}%)"
print(f"\n── {title} (всего уникальных: {total}) " + "" * 20)
print(f" покрытие v1: {pct(len(cov_v1))}")
print(f" покрытие v2: {pct(len(cov_v2))}")
print(f" есть в ОБОИХ (пересеч): {pct(len(both))}")
print(f" только v1: {len(only_v1)}")
print(f" только v2: {len(only_v2)}")
if len(cov_v1) == len(cov_v2) == len(both):
print(" ✅ наборы совпадают → покрытие не влияет на сравнение")
else:
diff = abs(len(cov_v1) - len(cov_v2))
print(
f" ⚠️ расхождение: |v1v2| = {diff} картинок; "
f"для честного сравнения оценивайте на пересечении ({len(both)})"
)
def main(args: argparse.Namespace) -> None:
data_root = Path(args.data_root)
text_levels = list(args.text_levels)
with open(data_root / args.test_meta) as f:
pairs_meta = json.load(f)
# Дрон-запросы: уникальные имена картинок дрона в тесте.
drone_names = sorted({e["drone_img_name"] for e in pairs_meta})
# Спутниковая галерея: те же уникальные тайлы, что и в eval.
gallery_names, _ = collect_gallery_names(pairs_meta)
print("=" * 64)
print(f"COVERAGE CHECK | text_levels={text_levels} | {args.test_meta}")
print("=" * 64)
for view, names in (("drone", drone_names), ("satellite", gallery_names)):
desc_v1 = load_text_descriptions(args.captions_v1, view_type=view)
desc_v2 = load_text_descriptions(args.captions_v2, view_type=view)
_report_side(
f"{view.upper()} {'queries' if view == 'drone' else 'gallery'}",
names, desc_v1, desc_v2, text_levels,
)
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Сравнить покрытие captions v1 vs v2")
p.add_argument("--data_root", type=str, required=True)
p.add_argument("--test_meta", type=str, default="cross-area-drone2sate-test.json")
p.add_argument("--captions_v1", type=str, required=True)
p.add_argument("--captions_v2", type=str, required=True)
p.add_argument("--text_levels", nargs="+", default=["level1"])
return p.parse_args()
if __name__ == "__main__":
main(parse_args())