"""Сравнение ПОКРЫТИЯ 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" ⚠️ расхождение: |v1−v2| = {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())