Initial commit: add docs, requirements, and prep package
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
210
DenseUAV_Dataset_Analysis.md
Normal file
210
DenseUAV_Dataset_Analysis.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# АНАЛИЗ ДАТАСЕТА: DenseUAV
|
||||
|
||||
**Дата анализа:** 2026-05-09
|
||||
**Метод:** GitHub-репозиторий + статья arXiv + эмпирическая проверка локальных файлов на диске
|
||||
**Путь к данным (локально):** `/media/servml/SSD_2_2TB/datasets/cvgl_datasets/DenseUAV`
|
||||
|
||||
Источники:
|
||||
- Код/описание и структура: [Dmmm1997/DenseUAV](https://github.com/Dmmm1997/DenseUAV)
|
||||
- Статья: [arXiv:2201.09201](https://arxiv.org/abs/2201.09201)
|
||||
|
||||
---
|
||||
|
||||
## 1. МЕТАДАННЫЕ
|
||||
|
||||
| Поле | Значение |
|
||||
|------|----------|
|
||||
| Полное название | Vision-Based UAV Self-Positioning in Low-Altitude Urban Environments |
|
||||
| Название датасета | **DenseUAV** |
|
||||
| Авторы | Ming Dai, Enhui Zheng, Zhenhua Feng, Jiedong Zhuang, Wankou Yang |
|
||||
| Год, Venue | arXiv: 2022 (v1), 2023 (v2); IEEE Transactions on Image Processing (TIP), 2024 (по репозиторию) |
|
||||
| Задача | **UAV self-positioning / cross-view geo-localization**: retrieval “UAV-view query → satellite-view gallery” |
|
||||
| Модальности | UAV-view (drone) + Satellite-view (overhead) |
|
||||
| Тип данных | Реальные городские университетские кампусы (14 кампусов; по описанию из репозитория) |
|
||||
| Лицензия | В репозитории DenseUAV указан Apache-2.0 для кода; лицензия на данные явно не подтверждена в локальной копии (требует проверки первоисточника) |
|
||||
| Объём (локально, эмпирически) | **~16 GB** (train ~8.8 GB, test ~7.0 GB) |
|
||||
|
||||
---
|
||||
|
||||
## 2. ОБЩАЯ СТАТИСТИКА (из репозитория и проверено по локальным файлам)
|
||||
|
||||
### 2.1. Сводка по разбиениям
|
||||
|
||||
| Subset | UAV-view | Satellite-view | Classes/IDs | Кампусы (universities) |
|
||||
|--------|----------:|---------------:|------------:|------------------------:|
|
||||
| Training | **6,768** | **13,536** | **2,256** | 10 |
|
||||
| Query (test) | **2,331** | **4,662** | **777** | 4 |
|
||||
| Gallery (test) | **9,099** | **18,198** | **3,033** | 14 |
|
||||
|
||||
Примечания по локальной копии:
|
||||
- В `train/drone/` и `train/satellite/` присутствует **2256** папок-ID, что совпадает с числом training classes.
|
||||
- В `test/gallery_satellite/` присутствует **3033** папок-ID (gallery classes).
|
||||
- В сумме по папкам-ID локальная копия покрывает **3033** уникальных ID (объединение train и test gallery), что согласуется с таблицей “Gallery Classes = 3033”.
|
||||
|
||||
### 2.2. Размеры на диске (локально)
|
||||
|
||||
| Раздел | Объём |
|
||||
|--------|------:|
|
||||
| DenseUAV (всё) | **~16 GB** |
|
||||
| `train/` | ~8.8 GB |
|
||||
| `test/` | ~7.0 GB |
|
||||
|
||||
---
|
||||
|
||||
## 3. ПОСТАНОВКА ЗАДАЧИ (как используется датасет)
|
||||
|
||||
DenseUAV предназначен для **самопозиционирования БПЛА** по изображению “вид сверху вниз”:
|
||||
- **Query:** UAV-view (дроновое изображение).
|
||||
- **Gallery:** набор satellite-view изображений (обычно несколько высот/вариантов на тот же ID).
|
||||
- **Цель:** найти правильный ID (и/или ближайший satellite-кадр) по embedding similarity.
|
||||
|
||||
В статье дополнительно вводятся метрики, оценивающие не только retrieval, но и аспект позиционирования (см. раздел 8).
|
||||
|
||||
---
|
||||
|
||||
## 4. СТРУКТУРА ДАННЫХ (локально)
|
||||
|
||||
Эмпирически наблюдаемая структура:
|
||||
|
||||
```
|
||||
DenseUAV/
|
||||
├── Dense_GPS_ALL.txt
|
||||
├── Dense_GPS_train.txt
|
||||
├── Dense_GPS_test.txt
|
||||
├── train/
|
||||
│ ├── drone/
|
||||
│ │ └── 000001/
|
||||
│ │ ├── H80.JPG
|
||||
│ │ ├── H90.JPG
|
||||
│ │ └── H100.JPG
|
||||
│ └── satellite/
|
||||
│ └── 000001/
|
||||
│ ├── H80.tif
|
||||
│ ├── H80_old.tif
|
||||
│ ├── H90.tif
|
||||
│ ├── H90_old.tif
|
||||
│ ├── H100.tif
|
||||
│ └── H100_old.tif
|
||||
└── test/
|
||||
├── query_drone/
|
||||
│ └── 002256/
|
||||
│ ├── H80.JPG
|
||||
│ ├── H90.JPG
|
||||
│ └── H100.JPG
|
||||
└── gallery_satellite/
|
||||
└── 000001/
|
||||
├── H80.tif
|
||||
├── H80_old.tif
|
||||
├── H90.tif
|
||||
├── H90_old.tif
|
||||
├── H100.tif
|
||||
└── H100_old.tif
|
||||
```
|
||||
|
||||
Наблюдения:
|
||||
- Для каждого ID в drone-части лежит **3 изображения**: `H80.JPG`, `H90.JPG`, `H100.JPG` (высоты/варианты).
|
||||
- Для каждого ID в satellite-части лежит **6 GeoTIFF**: текущие `H80/H90/H100.tif` + версии `*_old.tif` (по именам файлов — альтернативная/предыдущая спутниковая версия).
|
||||
|
||||
---
|
||||
|
||||
## 5. АННОТАЦИИ И МЕТАДАННЫЕ
|
||||
|
||||
### 5.1. GPS-файлы
|
||||
|
||||
В локальной копии присутствуют:
|
||||
- `Dense_GPS_ALL.txt`
|
||||
- `Dense_GPS_train.txt`
|
||||
- `Dense_GPS_test.txt`
|
||||
|
||||
Формат строк (эмпирически, по первым строкам):
|
||||
|
||||
```
|
||||
<relative_path> E<longitude> N<latitude> <height>
|
||||
```
|
||||
|
||||
Пример:
|
||||
- `train/satellite/000001/H80.tif E120.38776294444445 N30.32413311111111 94.761`
|
||||
|
||||
Интерпретация:
|
||||
- **longitude** задан как `E...`
|
||||
- **latitude** задан как `N...`
|
||||
- последнее поле похоже на **высоту/altitude** (метры) или атрибут высоты точки съёмки/привязки (требует подтверждения по статье; в файле присутствуют значения около 90–95 для приведённых строк).
|
||||
|
||||
### 5.2. Явные пары “query→positive”
|
||||
|
||||
В текущей локальной структуре (как она лежит на диске) отсутствуют отдельные файлы вида `positive.json`/`semi_positive.json` (как в некоторых patch-based CVGL датасетах). Здесь соответствие задаётся через:
|
||||
- общий **ID (папка)** — как “класс”,
|
||||
- и, вероятно, через GPS-метаданные для оценки “позиционирования” (в терминах статьи).
|
||||
|
||||
---
|
||||
|
||||
## 6. ОРИГИНАЛЬНЫЙ SPLIT И КЛАССЫ
|
||||
|
||||
По описанию из репозитория:
|
||||
- train включает данные с **10** кампусов,
|
||||
- test (query) включает **4** кампуса,
|
||||
- gallery охватывает **14** кампусов.
|
||||
|
||||
Практически это означает:
|
||||
- **Gallery** больше и разнообразнее (3033 классов/ID), чем **Query** (777 классов/ID).
|
||||
- retrieval оценивается как поиск правильного ID в большой галерее (и/или правильного satellite-вида для данного query).
|
||||
|
||||
---
|
||||
|
||||
## 7. ОСОБЕННОСТИ И ПЛЮСЫ/МИНУСЫ (для выбора датасета)
|
||||
|
||||
### 7.1. Сильные стороны
|
||||
|
||||
- **Плотная съёмка** и ориентирование на задачу self-positioning (в отличие от “объектной” UAV geo-localization).
|
||||
- **Cross-view** (UAV↔satellite) с несколькими вариантами высоты (`H80/H90/H100`).
|
||||
- **Наличие GPS** в явном виде для спутниковых изображений (и/или для части данных), что помогает считать метрики в метрах/геоошибку.
|
||||
|
||||
### 7.2. Ограничения / вопросы к воспроизводимости
|
||||
|
||||
- **Лицензия данных**: в репозитории явно указана Apache-2.0 для кода, но лицензионный статус именно датасета нужно подтвердить по первоисточнику/странице распространения.
|
||||
- **Что означает поле “height” в GPS txt** (последнее число) — требует сверки с текстом статьи/документацией.
|
||||
- Наличие `*_old.tif` подразумевает разные версии спутникового вида (может быть полезно как domain shift, но важно понять смысл: “старые тайлы”, другой рендер, другой год и т.п.).
|
||||
|
||||
---
|
||||
|
||||
## 8. ПРОТОКОЛ ОЦЕНКИ (по статье/репозиторию)
|
||||
|
||||
Из репозитория:
|
||||
- для оценки retrieval используются **Recall@K** (типично \(K \in \{1,5,10\}\)).
|
||||
- для оценки позиционирования используется метрика **SDM@K** (введена в статье; отражает “насколько близко по координатам” находятся top-K результаты).
|
||||
|
||||
Команды/скрипты (из репозитория) для baseline:
|
||||
- обучение: `train.py` (через `train_test_local.sh`)
|
||||
- тест: `test.py`
|
||||
- Recall: `evaluate_gpu.py`
|
||||
- SDM: `evaluateDistance.py`
|
||||
|
||||
---
|
||||
|
||||
## 9. КЛЮЧЕВЫЕ ЧИСЛА ДЛЯ ЦИТИРОВАНИЯ
|
||||
|
||||
| Метрика | Значение |
|
||||
|---------|----------|
|
||||
| Train UAV-view | **6,768** |
|
||||
| Train Satellite-view | **13,536** |
|
||||
| Test Query UAV-view | **2,331** |
|
||||
| Test Gallery Satellite-view | **18,198** |
|
||||
| Classes (train) | **2,256** |
|
||||
| Classes (query) | **777** |
|
||||
| Classes (gallery) | **3,033** |
|
||||
| Кампусы | **14** (в сумме, по описанию из репозитория) |
|
||||
| Объём на диске (локально) | **~16 GB** |
|
||||
| Варианты высоты (по именам файлов) | **3**: `H80`, `H90`, `H100` |
|
||||
|
||||
---
|
||||
|
||||
## 10. ЧТО НУЖНО ДЛЯ ВАШЕГО PIPELINE (если приводить к “UAV-GeoLoc style”)
|
||||
|
||||
Если ваши модели/даталоадеры ожидают формат “query list + db list + positives”, то для DenseUAV минимально можно:
|
||||
- трактовать **ID как label/class** (классификация/metric learning),
|
||||
- сформировать `train_query.txt` как строки вида: `train/drone/<ID>/H*.JPG <label> train/satellite/<ID>/H*.tif ...`
|
||||
- сформировать `train_db.txt` как список всех satellite изображений (или только “не old” версии, если так решено).
|
||||
|
||||
При необходимости добавить “позиционирование”:
|
||||
- использовать координаты из `Dense_GPS_*.txt` и определить расстояние между предсказанным top-K и GT (в логике SDM из статьи).
|
||||
|
||||
60
README.md
Normal file
60
README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# UAVDense (DenseUAV) prepare
|
||||
|
||||
Утилита для сканирования локальной копии **DenseUAV** и генерации индекс‑файлов для retrieval/metric learning пайплайна.
|
||||
|
||||
## Что генерируется
|
||||
|
||||
В выходной директории создаются:
|
||||
- `index/train_db.txt` — список всех satellite изображений (DB / gallery для обучения)
|
||||
- `index/train_query.txt` — строки вида: `path label pos1 pos2 ...`
|
||||
- `index/test_db.txt` — список всех `test/gallery_satellite` изображений
|
||||
- `index/test_query.txt` — строки вида: `path label pos1 pos2 ...`
|
||||
- `stats/stats.json` — сводная статистика и проверки целостности
|
||||
- `gps/gps_{split}.csv` — распарсенные `Dense_GPS_*.txt`
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Запуск (для вашего пути)
|
||||
|
||||
```bash
|
||||
python3 -m uavdense_prepare \
|
||||
--root "/media/servml/SSD_2_2TB/datasets/cvgl_datasets/DenseUAV" \
|
||||
--out "./out_uavdense" \
|
||||
--exclude-old
|
||||
```
|
||||
|
||||
Опция `--exclude-old` исключает `*_old.tif` из DB и positive list.
|
||||
|
||||
## Режим как в UAV-VisLoc-prepare (создать новую папку с ресайзом)
|
||||
|
||||
Если хотите получить **новую папку датасета** с ресайзнутыми изображениями (и конвертацией спутника `tif -> png`), используйте `--dst`:
|
||||
|
||||
```bash
|
||||
python3 -m uavdense_prepare \
|
||||
--root "/media/servml/SSD_2_2TB/datasets/cvgl_datasets/DenseUAV" \
|
||||
--dst "/media/servml/SSD_2_2TB/datasets/cvgl_datasets/DenseUAV_processed_256" \
|
||||
--target-size 256 \
|
||||
--out "./out_uavdense_processed_256" \
|
||||
--exclude-old
|
||||
```
|
||||
|
||||
В этом режиме индексы будут указывать на файлы в `--dst`.
|
||||
|
||||
Для быстрой проверки можно ограничить обработку:
|
||||
|
||||
```bash
|
||||
python3 -m uavdense_prepare \
|
||||
--root "/media/servml/SSD_2_2TB/datasets/cvgl_datasets/DenseUAV" \
|
||||
--dst "./DenseUAV_processed_debug" \
|
||||
--target-size 256 \
|
||||
--limit-ids 5 \
|
||||
--out "./out_uavdense_debug" \
|
||||
--exclude-old
|
||||
```
|
||||
|
||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
numpy
|
||||
pillow
|
||||
coloredlogs
|
||||
tqdm
|
||||
4
uavdense_prepare/__init__.py
Normal file
4
uavdense_prepare/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
__all__ = ["__version__"]
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
83
uavdense_prepare/__main__.py
Normal file
83
uavdense_prepare/__main__.py
Normal file
@@ -0,0 +1,83 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import coloredlogs
|
||||
|
||||
from .gps import parse_dense_gps_txt, write_gps_csv
|
||||
from .indexing import build_indices
|
||||
from .processing import ProcessingOptions, process_denseuav_to_new_root
|
||||
|
||||
|
||||
def _p(s: str) -> Path:
|
||||
return Path(s).expanduser().resolve()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(
|
||||
prog="uavdense_prepare",
|
||||
description="Prepare DenseUAV (UAVDense): optionally build processed dataset + indices + stats",
|
||||
)
|
||||
ap.add_argument("--root", required=True, help="Path to raw DenseUAV root directory")
|
||||
ap.add_argument("--out", required=True, help="Output directory for indices/stats (always created)")
|
||||
ap.add_argument(
|
||||
"--dst",
|
||||
default=None,
|
||||
help="If set, creates a new processed dataset root (resized images, satellite tif->png).",
|
||||
)
|
||||
ap.add_argument("--target-size", type=int, default=256, help="Resize to NxN in --dst mode")
|
||||
ap.add_argument(
|
||||
"--limit-ids",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Debug option: in --dst mode process only first N IDs from each split",
|
||||
)
|
||||
ap.add_argument("--exclude-old", action="store_true", help="Exclude *_old.tif from DB/positives")
|
||||
ap.add_argument("--no-strict", action="store_true", help="Do not fail on missing files/ids (best-effort)")
|
||||
ap.add_argument("--log-level", default="INFO", help="Logging level (DEBUG, INFO, WARNING, ERROR)")
|
||||
args = ap.parse_args()
|
||||
|
||||
coloredlogs.install(level=args.log_level.upper(), fmt="%(asctime)s %(levelname)s %(message)s")
|
||||
log = logging.getLogger("uavdense_prepare")
|
||||
|
||||
root = _p(args.root)
|
||||
out = _p(args.out)
|
||||
strict = not args.no_strict
|
||||
dst = _p(args.dst) if args.dst else None
|
||||
|
||||
dataset_root_for_index = root
|
||||
satellite_ext = ".tif"
|
||||
|
||||
if dst is not None:
|
||||
log.info("Building processed dataset at %s", dst)
|
||||
opts = ProcessingOptions(target_size=args.target_size, exclude_old=args.exclude_old, limit_ids=args.limit_ids)
|
||||
process_denseuav_to_new_root(src_root=root, dst_root=dst, opts=opts, strict=strict)
|
||||
dataset_root_for_index = dst
|
||||
satellite_ext = ".png"
|
||||
|
||||
# GPS exports (optional files) from the dataset root we index
|
||||
gps_dir = out / "gps"
|
||||
for name in ("Dense_GPS_ALL.txt", "Dense_GPS_train.txt", "Dense_GPS_test.txt"):
|
||||
src = dataset_root_for_index / name
|
||||
if src.exists():
|
||||
log.info("Parsing GPS file %s", src)
|
||||
recs = parse_dense_gps_txt(src)
|
||||
out_csv = gps_dir / (name.replace(".txt", ".csv").lower())
|
||||
write_gps_csv(recs, out_csv)
|
||||
|
||||
log.info("Building indices from %s into %s", dataset_root_for_index, out)
|
||||
build_indices(
|
||||
dataset_root_for_index,
|
||||
out,
|
||||
exclude_old=args.exclude_old,
|
||||
strict=strict,
|
||||
satellite_ext=satellite_ext,
|
||||
)
|
||||
log.info("Done")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
BIN
uavdense_prepare/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
uavdense_prepare/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
uavdense_prepare/__pycache__/__main__.cpython-312.pyc
Normal file
BIN
uavdense_prepare/__pycache__/__main__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
uavdense_prepare/__pycache__/gps.cpython-312.pyc
Normal file
BIN
uavdense_prepare/__pycache__/gps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
uavdense_prepare/__pycache__/indexing.cpython-312.pyc
Normal file
BIN
uavdense_prepare/__pycache__/indexing.cpython-312.pyc
Normal file
Binary file not shown.
BIN
uavdense_prepare/__pycache__/processing.cpython-312.pyc
Normal file
BIN
uavdense_prepare/__pycache__/processing.cpython-312.pyc
Normal file
Binary file not shown.
47
uavdense_prepare/gps.py
Normal file
47
uavdense_prepare/gps.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GpsRecord:
|
||||
rel_path: str
|
||||
lon: float
|
||||
lat: float
|
||||
height: float
|
||||
|
||||
|
||||
def parse_dense_gps_txt(path: Path) -> list[GpsRecord]:
|
||||
"""
|
||||
DenseUAV формат строки (эмпирически):
|
||||
<rel_path> E<lon> N<lat> <height>
|
||||
пример:
|
||||
train/satellite/000001/H80.tif E120.387... N30.324... 94.761
|
||||
"""
|
||||
records: list[GpsRecord] = []
|
||||
with path.open("r", encoding="utf-8", errors="replace") as f:
|
||||
for i, line in enumerate(f, start=1):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) != 4:
|
||||
raise ValueError(f"Bad GPS line {path}:{i}: expected 4 fields, got {len(parts)}: {line!r}")
|
||||
rel_path, e_lon, n_lat, height_s = parts
|
||||
if not e_lon.startswith("E") or not n_lat.startswith("N"):
|
||||
raise ValueError(f"Bad GPS line {path}:{i}: expected E.. N.. fields: {line!r}")
|
||||
lon = float(e_lon[1:])
|
||||
lat = float(n_lat[1:])
|
||||
height = float(height_s)
|
||||
records.append(GpsRecord(rel_path=rel_path, lon=lon, lat=lat, height=height))
|
||||
return records
|
||||
|
||||
|
||||
def write_gps_csv(records: list[GpsRecord], out_csv: Path) -> None:
|
||||
out_csv.parent.mkdir(parents=True, exist_ok=True)
|
||||
with out_csv.open("w", encoding="utf-8") as f:
|
||||
f.write("rel_path,lon,lat,height\n")
|
||||
for r in records:
|
||||
f.write(f"{r.rel_path},{r.lon:.12f},{r.lat:.12f},{r.height:.6f}\n")
|
||||
|
||||
196
uavdense_prepare/indexing.py
Normal file
196
uavdense_prepare/indexing.py
Normal file
@@ -0,0 +1,196 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
DRONE_NAMES = ("H80.JPG", "H90.JPG", "H100.JPG")
|
||||
SAT_NAMES = ("H80.tif", "H90.tif", "H100.tif", "H80_old.tif", "H90_old.tif", "H100_old.tif")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DenseUavLayout:
|
||||
root: Path
|
||||
|
||||
@property
|
||||
def train_drone_dir(self) -> Path:
|
||||
return self.root / "train" / "drone"
|
||||
|
||||
@property
|
||||
def train_sat_dir(self) -> Path:
|
||||
return self.root / "train" / "satellite"
|
||||
|
||||
@property
|
||||
def test_query_drone_dir(self) -> Path:
|
||||
return self.root / "test" / "query_drone"
|
||||
|
||||
@property
|
||||
def test_gallery_sat_dir(self) -> Path:
|
||||
return self.root / "test" / "gallery_satellite"
|
||||
|
||||
|
||||
def _iter_ids(dir_path: Path) -> list[str]:
|
||||
if not dir_path.exists():
|
||||
return []
|
||||
ids = [p.name for p in dir_path.iterdir() if p.is_dir()]
|
||||
return sorted(ids)
|
||||
|
||||
|
||||
def _collect_files_for_id(id_dir: Path, expected_names: Iterable[str]) -> dict[str, str]:
|
||||
"""
|
||||
Returns mapping name -> relative path (posix) for files that exist.
|
||||
"""
|
||||
out: dict[str, str] = {}
|
||||
for name in expected_names:
|
||||
p = id_dir / name
|
||||
if p.exists():
|
||||
out[name] = p.as_posix()
|
||||
return out
|
||||
|
||||
|
||||
def _write_lines(path: Path, lines: Iterable[str]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8") as f:
|
||||
for line in lines:
|
||||
f.write(line.rstrip("\n") + "\n")
|
||||
|
||||
|
||||
def build_indices(
|
||||
root: Path,
|
||||
out_dir: Path,
|
||||
*,
|
||||
exclude_old: bool = False,
|
||||
strict: bool = True,
|
||||
satellite_ext: str = ".tif",
|
||||
) -> dict:
|
||||
"""
|
||||
Создаёт train/test индексы в стиле:
|
||||
query_path label pos1 pos2 ...
|
||||
где label = целочисленный id класса (по порядку).
|
||||
|
||||
Positive list:
|
||||
- для train query: все доступные satellite варианты того же ID (3 или 6 файлов)
|
||||
- для test query: satellite варианты из test/gallery_satellite/<ID> (если есть)
|
||||
"""
|
||||
layout = DenseUavLayout(root=root)
|
||||
|
||||
# IDs
|
||||
train_ids = _iter_ids(layout.train_drone_dir)
|
||||
train_sat_ids = _iter_ids(layout.train_sat_dir)
|
||||
if strict and train_ids != train_sat_ids:
|
||||
raise ValueError("train/drone IDs differ from train/satellite IDs")
|
||||
|
||||
gallery_ids = _iter_ids(layout.test_gallery_sat_dir)
|
||||
query_ids = _iter_ids(layout.test_query_drone_dir)
|
||||
|
||||
# label mapping: use gallery id universe for stable evaluation labels
|
||||
# (train ids are subset of gallery ids in typical setting)
|
||||
all_ids = sorted(set(gallery_ids) | set(train_ids) | set(query_ids))
|
||||
id_to_label = {id_: i for i, id_ in enumerate(all_ids)}
|
||||
|
||||
# Collect DB lists
|
||||
def sat_paths_for(id_: str, sat_root: Path) -> list[str]:
|
||||
id_dir = sat_root / id_
|
||||
if not id_dir.exists():
|
||||
return []
|
||||
names = list(SAT_NAMES)
|
||||
if exclude_old:
|
||||
names = [n for n in names if not n.endswith("_old.tif")]
|
||||
# allow processed datasets where tif were converted to png
|
||||
names_fs = [n.replace(".tif", satellite_ext) for n in names]
|
||||
got = _collect_files_for_id(id_dir, names_fs)
|
||||
# stable order: H80,H90,H100,(old...)
|
||||
ordered = [got[n] for n in names_fs if n in got]
|
||||
# make relative to dataset root
|
||||
rel = [str(Path(p).relative_to(root).as_posix()) for p in ordered]
|
||||
return rel
|
||||
|
||||
def drone_paths_for(id_: str, drone_root: Path) -> list[str]:
|
||||
id_dir = drone_root / id_
|
||||
if not id_dir.exists():
|
||||
return []
|
||||
got = _collect_files_for_id(id_dir, DRONE_NAMES)
|
||||
ordered = [got[n] for n in DRONE_NAMES if n in got]
|
||||
rel = [str(Path(p).relative_to(root).as_posix()) for p in ordered]
|
||||
return rel
|
||||
|
||||
# train_db: all train satellite images (optionally exclude old)
|
||||
train_db: list[str] = []
|
||||
for id_ in train_ids:
|
||||
train_db.extend(sat_paths_for(id_, layout.train_sat_dir))
|
||||
|
||||
# test_db: all test gallery satellite images
|
||||
test_db: list[str] = []
|
||||
for id_ in gallery_ids:
|
||||
test_db.extend(sat_paths_for(id_, layout.test_gallery_sat_dir))
|
||||
|
||||
# Queries:
|
||||
train_query_lines: list[str] = []
|
||||
train_missing: list[str] = []
|
||||
for id_ in train_ids:
|
||||
q_paths = drone_paths_for(id_, layout.train_drone_dir)
|
||||
pos = sat_paths_for(id_, layout.train_sat_dir)
|
||||
if strict:
|
||||
if len(q_paths) != 3:
|
||||
train_missing.append(f"{id_}: drone files {len(q_paths)}/3")
|
||||
if (exclude_old and len(pos) != 3) or ((not exclude_old) and len(pos) != 6):
|
||||
train_missing.append(f"{id_}: satellite files {len(pos)}/expected")
|
||||
for q in q_paths:
|
||||
label = id_to_label[id_]
|
||||
if not pos:
|
||||
if strict:
|
||||
raise ValueError(f"No positives for train query id={id_}")
|
||||
continue
|
||||
train_query_lines.append(" ".join([q, str(label), *pos]))
|
||||
|
||||
test_query_lines: list[str] = []
|
||||
test_missing: list[str] = []
|
||||
for id_ in query_ids:
|
||||
q_paths = drone_paths_for(id_, layout.test_query_drone_dir)
|
||||
pos = sat_paths_for(id_, layout.test_gallery_sat_dir)
|
||||
if strict:
|
||||
if len(q_paths) != 3:
|
||||
test_missing.append(f"{id_}: query files {len(q_paths)}/3")
|
||||
if not pos:
|
||||
test_missing.append(f"{id_}: no gallery satellite folder/files")
|
||||
for q in q_paths:
|
||||
label = id_to_label[id_]
|
||||
if not pos:
|
||||
if strict:
|
||||
raise ValueError(f"No positives for test query id={id_}")
|
||||
continue
|
||||
test_query_lines.append(" ".join([q, str(label), *pos]))
|
||||
|
||||
# Write outputs
|
||||
index_dir = out_dir / "index"
|
||||
_write_lines(index_dir / "train_db.txt", train_db)
|
||||
_write_lines(index_dir / "test_db.txt", test_db)
|
||||
_write_lines(index_dir / "train_query.txt", train_query_lines)
|
||||
_write_lines(index_dir / "test_query.txt", test_query_lines)
|
||||
|
||||
stats = {
|
||||
"root": str(root),
|
||||
"out_dir": str(out_dir),
|
||||
"exclude_old": exclude_old,
|
||||
"strict": strict,
|
||||
"counts": {
|
||||
"n_train_ids": len(train_ids),
|
||||
"n_gallery_ids": len(gallery_ids),
|
||||
"n_query_ids": len(query_ids),
|
||||
"n_all_ids_universe": len(all_ids),
|
||||
"n_train_db_images": len(train_db),
|
||||
"n_test_db_images": len(test_db),
|
||||
"n_train_query_images": len(train_query_lines),
|
||||
"n_test_query_images": len(test_query_lines),
|
||||
},
|
||||
"integrity": {
|
||||
"train_issues": train_missing[:200],
|
||||
"test_issues": test_missing[:200],
|
||||
},
|
||||
}
|
||||
(out_dir / "stats").mkdir(parents=True, exist_ok=True)
|
||||
(out_dir / "stats" / "stats.json").write_text(json.dumps(stats, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
return stats
|
||||
|
||||
173
uavdense_prepare/processing.py
Normal file
173
uavdense_prepare/processing.py
Normal file
@@ -0,0 +1,173 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
from tqdm import tqdm
|
||||
|
||||
from .indexing import DenseUavLayout, DRONE_NAMES, SAT_NAMES
|
||||
|
||||
|
||||
log = logging.getLogger("uavdense_prepare")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProcessingOptions:
|
||||
target_size: int = 256
|
||||
exclude_old: bool = False
|
||||
jpeg_quality: int = 95
|
||||
limit_ids: int | None = None
|
||||
|
||||
|
||||
def _ensure_dir(p: Path) -> None:
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def _resize_to_square(img: Image.Image, target_size: int) -> Image.Image:
|
||||
if img.mode not in ("RGB", "L"):
|
||||
img = img.convert("RGB")
|
||||
return img.resize((target_size, target_size), Image.LANCZOS)
|
||||
|
||||
|
||||
def _copy_resize_jpg(src: Path, dst: Path, target_size: int, quality: int) -> None:
|
||||
if dst.exists():
|
||||
return
|
||||
with Image.open(src) as im:
|
||||
out = _resize_to_square(im, target_size)
|
||||
_ensure_dir(dst.parent)
|
||||
out.save(dst, "JPEG", quality=quality)
|
||||
|
||||
|
||||
def _copy_resize_tif_to_png(src: Path, dst: Path, target_size: int) -> None:
|
||||
if dst.exists():
|
||||
return
|
||||
with Image.open(src) as im:
|
||||
out = _resize_to_square(im, target_size)
|
||||
_ensure_dir(dst.parent)
|
||||
out.save(dst, "PNG")
|
||||
|
||||
|
||||
def _iter_ids(dir_path: Path) -> list[str]:
|
||||
if not dir_path.exists():
|
||||
return []
|
||||
ids = [p.name for p in dir_path.iterdir() if p.is_dir()]
|
||||
return sorted(ids)
|
||||
|
||||
|
||||
def process_denseuav_to_new_root(
|
||||
*,
|
||||
src_root: Path,
|
||||
dst_root: Path,
|
||||
opts: ProcessingOptions,
|
||||
strict: bool = True,
|
||||
) -> dict:
|
||||
"""
|
||||
Создаёт новую структуру датасета в dst_root:
|
||||
- drone: JPEG resized до target_size
|
||||
- satellite: PNG resized до target_size (из .tif)
|
||||
|
||||
Важно: DenseUAV уже “разрезан” по ID, поэтому satellite tiling не требуется.
|
||||
"""
|
||||
src = DenseUavLayout(root=src_root)
|
||||
dst = DenseUavLayout(root=dst_root)
|
||||
|
||||
# Validate IDs
|
||||
train_ids = _iter_ids(src.train_drone_dir)
|
||||
train_sat_ids = _iter_ids(src.train_sat_dir)
|
||||
if strict and train_ids != train_sat_ids:
|
||||
raise ValueError("train/drone IDs differ from train/satellite IDs")
|
||||
|
||||
test_query_ids = _iter_ids(src.test_query_drone_dir)
|
||||
test_gallery_ids = _iter_ids(src.test_gallery_sat_dir)
|
||||
|
||||
if opts.limit_ids is not None:
|
||||
train_ids = train_ids[: opts.limit_ids]
|
||||
# For test, keep query IDs (first N) and ensure gallery contains them,
|
||||
# otherwise indexing will not find positives.
|
||||
test_query_ids = test_query_ids[: opts.limit_ids]
|
||||
gallery_set = set(test_gallery_ids)
|
||||
test_gallery_ids = [i for i in test_query_ids if i in gallery_set]
|
||||
|
||||
issues: list[str] = []
|
||||
copied = {"train_drone": 0, "train_sat": 0, "test_query_drone": 0, "test_gallery_sat": 0}
|
||||
|
||||
# Process train drone
|
||||
log.info("Processing train drone (%d IDs)", len(train_ids))
|
||||
for id_ in tqdm(train_ids, desc="train/drone IDs"):
|
||||
for name in DRONE_NAMES:
|
||||
s = src.train_drone_dir / id_ / name
|
||||
d = dst.train_drone_dir / id_ / name
|
||||
if not s.exists():
|
||||
if strict:
|
||||
raise FileNotFoundError(s)
|
||||
issues.append(f"missing: {s}")
|
||||
continue
|
||||
_copy_resize_jpg(s, d, opts.target_size, opts.jpeg_quality)
|
||||
copied["train_drone"] += 1
|
||||
|
||||
# Process train satellite (.tif -> .png)
|
||||
sat_names = list(SAT_NAMES)
|
||||
if opts.exclude_old:
|
||||
sat_names = [n for n in sat_names if not n.endswith("_old.tif")]
|
||||
|
||||
log.info("Processing train satellite (%d IDs)", len(train_ids))
|
||||
for id_ in tqdm(train_ids, desc="train/satellite IDs"):
|
||||
for name in sat_names:
|
||||
s = src.train_sat_dir / id_ / name
|
||||
d = dst.train_sat_dir / id_ / name.replace(".tif", ".png")
|
||||
if not s.exists():
|
||||
if strict:
|
||||
raise FileNotFoundError(s)
|
||||
issues.append(f"missing: {s}")
|
||||
continue
|
||||
_copy_resize_tif_to_png(s, d, opts.target_size)
|
||||
copied["train_sat"] += 1
|
||||
|
||||
# Process test query drone
|
||||
log.info("Processing test query drone (%d IDs)", len(test_query_ids))
|
||||
for id_ in tqdm(test_query_ids, desc="test/query_drone IDs"):
|
||||
for name in DRONE_NAMES:
|
||||
s = src.test_query_drone_dir / id_ / name
|
||||
d = dst.test_query_drone_dir / id_ / name
|
||||
if not s.exists():
|
||||
if strict:
|
||||
raise FileNotFoundError(s)
|
||||
issues.append(f"missing: {s}")
|
||||
continue
|
||||
_copy_resize_jpg(s, d, opts.target_size, opts.jpeg_quality)
|
||||
copied["test_query_drone"] += 1
|
||||
|
||||
# Process test gallery satellite
|
||||
log.info("Processing test gallery satellite (%d IDs)", len(test_gallery_ids))
|
||||
for id_ in tqdm(test_gallery_ids, desc="test/gallery_satellite IDs"):
|
||||
for name in sat_names:
|
||||
s = src.test_gallery_sat_dir / id_ / name
|
||||
d = dst.test_gallery_sat_dir / id_ / name.replace(".tif", ".png")
|
||||
if not s.exists():
|
||||
if strict:
|
||||
raise FileNotFoundError(s)
|
||||
issues.append(f"missing: {s}")
|
||||
continue
|
||||
_copy_resize_tif_to_png(s, d, opts.target_size)
|
||||
copied["test_gallery_sat"] += 1
|
||||
|
||||
# Also copy GPS txt as-is (if present)
|
||||
for name in ("Dense_GPS_ALL.txt", "Dense_GPS_train.txt", "Dense_GPS_test.txt"):
|
||||
s = src_root / name
|
||||
if s.exists():
|
||||
d = dst_root / name
|
||||
_ensure_dir(d.parent)
|
||||
if not d.exists():
|
||||
d.write_text(s.read_text(encoding="utf-8", errors="replace"), encoding="utf-8")
|
||||
|
||||
# Minimal marker
|
||||
(dst_root / ".prepared_by_uavdense_prepare.txt").write_text(
|
||||
f"target_size={opts.target_size}\nexclude_old={opts.exclude_old}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
log.info("Processing completed. Copied: %s", copied)
|
||||
return {"copied": copied, "issues": issues[:200], "dst_root": str(dst_root)}
|
||||
|
||||
Reference in New Issue
Block a user