Compare commits
28 Commits
339e5f210c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| fc072d798e | |||
| 2ec0763e6d | |||
| 3372ee4055 | |||
| b4b8f78970 | |||
| daee1767fb | |||
| fa4c4b83ae | |||
| ec8b3ae20e | |||
| b2cc714d79 | |||
| 703ea8dbaf | |||
| 4b398f6c9a | |||
| 702c53caac | |||
| 15cad7fa65 | |||
| 2561ba1cbf | |||
| 26f2fac07b | |||
| 2b137dfccc | |||
| a386cfa614 | |||
| 8a7d88155d | |||
| c6df3edab8 | |||
| 05f8746d58 | |||
| 43cd4222bc | |||
| 1de150b386 | |||
| 178f2a1b9c | |||
| 9b685c9a88 | |||
| a9bde5e5b9 | |||
| 0cc210968f | |||
| 27d5c6eaab | |||
| 6040f3b253 | |||
| 6d4208d100 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
.venv
|
||||
__pycache__
|
||||
*.png
|
||||
images
|
||||
trajectories
|
||||
z
|
||||
chunks/*
|
||||
images/*
|
||||
2
datasets/ya_go_maps/.gitignore
vendored
Normal file
2
datasets/ya_go_maps/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
images
|
||||
dataset_*
|
||||
66
datasets/ya_go_maps/extract.py
Normal file
66
datasets/ya_go_maps/extract.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import argparse
|
||||
import os
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def extract_zip_to_images(dname):
|
||||
"""
|
||||
Разархивирует {dname}.zip в папку images.
|
||||
"""
|
||||
# Определяем пути
|
||||
current_dir = Path(__file__).parent
|
||||
zip_path = current_dir / f"{dname}.zip"
|
||||
extract_dir = current_dir
|
||||
|
||||
# Проверяем существование архива
|
||||
if not zip_path.exists():
|
||||
print(f"Ошибка: архив не найден: {zip_path}")
|
||||
return False
|
||||
|
||||
# Создаем папку для извлечения, если она не существует
|
||||
extract_dir.mkdir(exist_ok=True)
|
||||
|
||||
try:
|
||||
# Открываем архив
|
||||
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
||||
# Получаем список файлов в архиве
|
||||
file_list = zip_ref.namelist()
|
||||
print(f"Найдено {len(file_list)} файлов в архиве")
|
||||
|
||||
# Извлекаем все файлы
|
||||
zip_ref.extractall(extract_dir)
|
||||
|
||||
# Получаем список извлеченных файлов
|
||||
extracted_files = list(extract_dir.rglob("*"))
|
||||
image_files = [f for f in extracted_files if f.is_file()]
|
||||
|
||||
print(f"Успешно извлечено {len(image_files)} файлов в {extract_dir}")
|
||||
return True
|
||||
|
||||
except zipfile.BadZipFile:
|
||||
print(f"Ошибка: архив поврежден или не является ZIP-файлом: {zip_path}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Ошибка при разархивировании: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Основная функция для запуска разархивирования.
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--dname', type=str, default='ya_go_maps', help='Dataset name')
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"Начинаю разархивирование {args.dname}.zip...")
|
||||
|
||||
if extract_zip_to_images(args.dname):
|
||||
print("Разархивирование успешно завершено!")
|
||||
else:
|
||||
print("Разархивирование не удалось.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
57
datasets/ya_go_maps/generate_dataset.py
Normal file
57
datasets/ya_go_maps/generate_dataset.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import argparse
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from google_map import GoogleMap
|
||||
from simulator import Simulator
|
||||
from yandex_map import YandexMap
|
||||
from constants import CHUNK_WIDTH
|
||||
|
||||
LAT_MIN, LAT_MAX = 49.134520, 49.235065
|
||||
LON_MIN, LON_MAX = 55.767660, 55.825204
|
||||
|
||||
|
||||
|
||||
def create_new_asset(yandex_map, google_map, dname, tile_y=None, tile_x=None):
|
||||
folder = Path(f'dataset_{dname}')
|
||||
|
||||
id = 0
|
||||
print(id)
|
||||
while (folder / f"{id:0{4}}_google.png").exists():
|
||||
id += 1
|
||||
|
||||
google_file = folder / f"{id:0{4}}_google.png"
|
||||
yandex_file = folder / f"{id:0{4}}_yandex.png"
|
||||
|
||||
lat = np.random.rand() * (LAT_MAX - LAT_MIN) + LAT_MIN
|
||||
lon = np.random.rand() * (LON_MAX - LON_MIN) + LON_MIN
|
||||
if tile_y is not None and tile_x is not None:
|
||||
lat = tile_y * (LAT_MAX - LAT_MIN) + LAT_MIN
|
||||
lon = tile_x * (LON_MAX - LON_MIN) + LON_MIN
|
||||
|
||||
yandex_map.open(lat, lon, 18)
|
||||
google_map.open(lat, lon, 18)
|
||||
|
||||
simulator = Simulator()
|
||||
im_ya = simulator._apply_perspective_transform(yandex_map.make_screenshot())
|
||||
im_go = simulator._apply_perspective_transform(google_map.make_screenshot())
|
||||
im_ya.resize((CHUNK_WIDTH // 2, CHUNK_WIDTH // 2)).save(yandex_file)
|
||||
im_go.resize((CHUNK_WIDTH // 2, CHUNK_WIDTH // 2)).save(google_file)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--dname', type=str, default='ya_go_maps', help='Dataset name')
|
||||
args = parser.parse_args()
|
||||
|
||||
folder = Path(f'dataset_{args.dname}')
|
||||
if not folder.exists():
|
||||
folder.mkdir()
|
||||
|
||||
yandex_map = YandexMap(initial_zoom=18)
|
||||
google_map = GoogleMap(initial_zoom=18)
|
||||
|
||||
for tile_y in np.linspace(0, 1, 20):
|
||||
for tile_x in np.linspace(0, 1, 20):
|
||||
create_new_asset(yandex_map, google_map, args.dname, tile_y, tile_x)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
BIN
datasets/ya_go_maps/ya_go_maps_v2.zip
Normal file
BIN
datasets/ya_go_maps/ya_go_maps_v2.zip
Normal file
Binary file not shown.
1
dissertation/_media/readme.md
Normal file
1
dissertation/_media/readme.md
Normal file
@@ -0,0 +1 @@
|
||||
Здесь хранятся медиафайлы (картинки, схемы)
|
||||
9
dissertation/_todo_backlog.md
Normal file
9
dissertation/_todo_backlog.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Backlog предложений
|
||||
|
||||
## Предложения по улучшению
|
||||
|
||||
- Добавить экспериментальные результаты для каждого алгоритма
|
||||
- Сравнить производительность разных подходов к гомографии
|
||||
- Исследовать влияние освещения на точность сопоставления
|
||||
- Добавить раздел о практическом применении разработанного алгоритма
|
||||
- Рассмотреть возможность использования других архитектур нейросетей
|
||||
12
dissertation/chapter_1/chapter_1_1.md
Normal file
12
dissertation/chapter_1/chapter_1_1.md
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
1.1 Задача возврата в точку старта в системах навигации БПЛА
|
||||
|
||||
Классическая навигация БПЛА основана на данных инерциальной навигационной системы (ИНС), спутниковых приёмников и, при необходимости, барометрических и радиотехнических средств. Такие комплексы обеспечивают приемлемую точность в стандартных условиях, но становятся уязвимыми при глушении или маскировании навигационных сигналов, а также при полётах в помещениях, городских «каньонах» и под навесами, где спутниковые сигналы физически недоступны.
|
||||
В ответ на эти ограничения активно развиваются альтернативные средства навигации, в частности оптико‑электронные системы, использующие бортовую видеокамеру и корреляционную обработку изображений. Для малых БПЛА особое значение имеют вычислительные и массогабаритные ограничения, что требует применения алгоритмов с невысокой вычислительной сложностью и малым объёмом памяти при сохранении требуемой точности позиционирования.
|
||||
В этом контексте отдельный интерес представляет задача возврата аппарата в начальную точку полёта в условиях отсутствия внешних навигационных сигналов. Типичный сценарий выглядит следующим образом: на этапе прямого полёта от старта к целевой области бортовая система формирует некоторое эталонное представление маршрута (последовательность кадров, фрагменты карты местности, 3D‑модель), а при потере внешней навигации или по команде оператора активируется режим автономного возврата. В этом режиме по текущему видеопотоку и частично известной карте необходимо корректировать курс и ориентирование БПЛА таким образом, чтобы аппарат вновь оказался в окрестности точки старта с допустимой погрешностью по координатам.
|
||||
Рассмотрим типичный сценарий, лежащий в основе постановки данной работы:
|
||||
Этап полёта «туда»: БПЛА стартует из начальной позиции p_0 и летит к целевой области. На этом этапе система сохраняет информацию о пройденном маршруте (последовательность видеокадров, карта местности или набор 3D-признаков). Спутниковая навигация доступна или используется периодически.
|
||||
Потеря внешней навигации: при достижении целевой области БПЛА теряет спутниковый сигнал (или оператор отключает его по команде) либо входит в режим автономного полёта, когда внешняя связь перестаёт быть достоверным источником информации.
|
||||
Режим возврата: активируется режим автономного возврата, в котором БПЛА должен вернуться в исходную позицию p_0 с минимальной ошибкой, опираясь только на данные бортовой видеокамеры (нижняя ориентация, смотрит вертикально вниз), ранее накопленную информацию о маршруте (карта или видеопоследовательность), при необходимости данные ИНС (гироскопы, акселерометры).
|
||||
В литературе выделяются несколько направлений, в рамках которых рассматривается подобная постановка. К ним относятся методы на основе визуальной одометрии и SLAM, в которых построение карты и локализация выполняются совместно в процессе полёта, алгоритмы корреляционно‑экстремальной навигации по заранее подготовленным или формируемым во время полета эталонным картам, а также специализированные алгоритмы сопоставления видеопоследовательностей «туда» и «обратно», ориентированные непосредственно на реализацию режима «возврат в точку старта» [3].
|
||||
В данной главе рассматриваются основные из этих подходов, анализируются их достоинства и ограничения применительно к малым БПЛА, а также формулируются требования к алгоритмам, ориентированным на работу в условиях ограниченных вычислительных ресурсов и возможного рассогласования между эталонной картой и текущими наблюдениями. Подробная формальная постановка задачи и выбор конкретных методов решения приводятся во второй главе.
|
||||
8
dissertation/chapter_1/chapter_1_2.md
Normal file
8
dissertation/chapter_1/chapter_1_2.md
Normal file
@@ -0,0 +1,8 @@
|
||||
1.2 Классификация методов навигации БПЛА
|
||||
|
||||
С точки зрения используемых источников информации и постановки задачи возврата в исходную точку навигационные алгоритмы для БПЛА можно условно разбить на несколько групп [1]:
|
||||
интегрированные ИНС + спутниковые системы (GPS/ГЛОНАСС): используют данные инерциальной навигационной системы, корректируемые спутниковыми измерениями; возврат в точку старта в этом случае сводится к следованию по заранее вычисленной траектории в глобальных координатах, но система полностью теряет устойчивость при глушении/маскировании спутниковых сигналов [8];
|
||||
визуальные и визуально‑инерциальные системы (VO/VIO): оценивают траекторию и ориентацию по видеопотоку и данным IMU, в ряде случаев строя карту окружающего пространства; могут обеспечивать возврат по оценённой траектории или по ранее построенной карте [15];
|
||||
корреляционно‑экстремальные навигационные системы (КЭНС): используют сопоставление текущих изображений с эталонной цифровой картой (2D или 3D) для коррекции ИНС и реализации возврата по видовой информации; позволяют работать даже при сильных ограничениях на вычислительные ресурсы [3].
|
||||
Специализированные алгоритмы «возврата по видео» сравнивают текущую видеопоследовательность с ранее записанной при полёте по маршруту «туда» и по максимуму меры сходства определяют положение БПЛА вдоль маршрута и направление движения к старту.
|
||||
Для каждой группы используются свои ключевые метрики: для VO/VIO – ошибки траектории (ATE/RPE), для КЭНС — точность привязки к карте и время обработки кадра, для алгоритмов возврата — погрешность конечного положения относительно точки старта и вероятность успешного завершения возврата.
|
||||
7
dissertation/chapter_1/chapter_1_3.md
Normal file
7
dissertation/chapter_1/chapter_1_3.md
Normal file
@@ -0,0 +1,7 @@
|
||||
1.3 Интегрированные инерциально‑спутниковые навигационные системы
|
||||
|
||||
Интегрированные инерциально‑спутниковые навигационные комплексы (ИНС/ГНСС) являются классическим и наиболее распространённым решением для навигации БПЛА, на которое опираются как серийные автопилоты, так и специализированные системы управления полётом. В типичном случае такой комплекс включает инерциальный модуль (акселерометры и гироскопы, часто в составе МЭМС‑IMU), приёмник глобальной навигационной спутниковой системы (GPS/ГЛОНАСС и др.), а также барометрический высотомер и магнитометр, которые используются для уточнения высоты и курса. Инерциальная система обеспечивает высокочастотное счисление пути — оценку координат, скорости и ориентации путём интегрирования измеренных ускорений и угловых скоростей, тогда как ГНСС выдаёт абсолютные координаты и скорость в глобальной системе, компенсируя накопление ошибок ИНС на длинных интервалах времени.
|
||||
Поскольку автономная ИНС подвержена дрейфу из‑за шума и систематических смещений датчиков, в интегрированных комплексах применяется алгоритмическая «сшивка» двух подсистем, чаще всего на основе расширенного фильтра Калмана или близких к нему рекуррентных оценивателей. В рамках такой фильтрационной схемы инерциальная часть играет роль модели движения (прогноза), а спутниковые измерения рассматриваются как внешние наблюдения, периодически корректирующие состояние фильтра и тем самым ограничивающие рост погрешности. На практике используются различные варианты интеграции – от «слабосвязанной», когда в фильтр поступает уже вычисленное решением ГНСС положение, до более тесной, где обрабатываются сырые псевдодальности и доплеровские измерения; при этом для малых БПЛА обычно выбирают схемы, обеспечивающие приемлемый компромисс между точностью и вычислительной сложностью.
|
||||
В контексте задачи возврата в точку старта интегрированная система ИНС/ГНСС позволяет на участке от взлёта до целевой области формировать траекторию полёта в глобальных координатах и запоминать положение начальной точки. При наличии устойчивого спутникового сигнала возврат реализуется в виде режима, аналогичного режиму Return‑to‑Home в коммерческих автопилотах: бортовой контроллер либо следует по заранее заданному маршруту в обратном направлении, либо строит прямую траекторию к сохранённым координатам точки старта, используя текущие оценки положения и ориентации от интегрированного навигационного комплекса. Кратковременные выпадения или ухудшение качества ГНСС‑сигнала (затенение антенны, частичные помехи) в этом случае «переживаются» за счёт инерциальной части системы: ИНС обеспечивает непрерывность навигационного решения между обновлениями от спутников и предотвращает резкие скачки координат.
|
||||
Однако в условиях, представляющих наибольший интерес для данной работы, — полётах в городских «каньонах», в помещениях, под перекрытиями, а также в зонах радиоэлектронного подавления — интегрированные ИНС/ГНСС‑комплексы демонстрируют принципиальные ограничения. При полном отсутствии или сильной деградации спутниковых сигналов навигационное решение фактически сводится к автономной ИНС: ошибка положения начинает расти во времени, и уже через относительно короткий промежуток полёта погрешности достигают величин, сопоставимых или превышающих допустимый радиус зоны возврата. Дополнительно на точность влияют многолучёвость и отражения сигналов в городской застройке, возможные атаки типа спуфинга и глушения, а также массогабаритные и энергетические ограничения малых БПЛА, не позволяющие использовать высокоточные (и дорогостоящие) инерциальные датчики и профессиональные ГНСС‑приёмники.
|
||||
В результатe интегрированные ИНС/ГНСС‑системы обеспечивают надёжный и технологически отработанный механизм навигации и возврата в точку старта при наличии работоспособного спутникового канала, однако оказываются недостаточными в сценариях, где этот канал временно или полностью недоступен. Именно поэтому в современных исследованиях всё большее внимание уделяется альтернативным подходам — визуальной и визуально‑инерциальной одометрии, а также корреляционно‑экстремальной навигации по видовой информации, которые потенциально позволяют реализовать режим возврата в точку старта при отсутствии GPS/ГЛОНАСС и рассматриваются в последующих подпунктах обзора.
|
||||
9
dissertation/chapter_1/chapter_1_4.md
Normal file
9
dissertation/chapter_1/chapter_1_4.md
Normal file
@@ -0,0 +1,9 @@
|
||||
1.4 Визуальная и визуально-инерциальная одометрия для автономной навигации
|
||||
|
||||
Визуальная одометрия (VO) и визуально‑инерциальная одометрия (VIO) решают задачу оценивания траектории носителя по последовательности изображений и данным ИНС, используя методы извлечения и сопоставления признаков, построения карт и оптимизационной фильтрации.
|
||||
Примером аппаратно ориентированного VIO‑решения является чип «Navion», представляющий собой энергоэффективный ускоритель визуально‑инерциальной одометрии для автономной навигации нано‑дронов [19]. В «Navion» вся VIO‑цепочка реализована на специализированной ASIC‑микросхеме: фронтенд обрабатывает моно-изображения или стереоизображения (детекция углов Харриса, трекинг по Лукасу–Канаде, стерео-сопоставление), а инерциальный фронтенд выполняет предварительное интегрирование данных IMU; на бэкенде решается нелинейная задача минимизации на фактор‑графе для оценки траектории и разреженной 3D‑карты.
|
||||
Для уменьшения энергопотребления и объёма памяти в «Navion» используются сжатие изображений с блочно‑пороговой квантизацией, разрежённые структуры для хранения треков признаков и разрежённые представления матрицы Гессе в линейном решателе. Это позволяет обрабатывать стереокадры разрешения 752×480 с частотой до сотен кадров в секунду при средней потребляемой мощности порядка десятков милливатт, обеспечивая при этом среднюю ошибку траектории менее процента на сложных наборах данных для мультикоптеров. Такой подход демонстрирует, что VIO‑алгоритмы могут быть реализованы на бортовой аппаратуре малых БПЛА при строгих ограничениях по ресурсоёмкости.
|
||||
Отдельный класс решений составляют фильтрационные VIO‑алгоритмы, в которых оценивание состояния (координаты, скорость, ориентация, смещения датчиков) выполняется с помощью расширенного фильтра Калмана или его модификаций [15]. В работе SP‑VIO предлагается фильтрационный VIO‑алгоритм, ориентированный на миниатюрные робототехнические платформы и условия деградации визуальной информации.
|
||||
Авторы SP‑VIO показывают недостатки традиционных фильтров (например, MSCKF): накопление ошибок и проблемы с учётом наблюдаемости. Для решения используется двойная трансформация состояния (DST‑EKF) с улучшенной моделью визуальных измерений, которая полностью разделяет ошибки скорости/положения и визуальные остатки.
|
||||
На популярных публичных датасетах (EuRoC, Tum‑VI, KITTI) SP‑VIO демонстрирует более низкую среднеквадратическую ошибку траектории по сравнению с рядом современных VIO‑алгоритмов (VINS‑Mono, OpenVINS и др.) при сопоставимой или лучшей вычислительной эффективности. Дополнительно реализована стратегия сглаживания траектории DST‑RTS, позволяющая существенно уменьшить накопленную ошибку при длительных потерях визуальных наблюдений и тем самым повысить робастность навигации в условиях временных «провалов» видеосигнала.
|
||||
Для решаемой в диссертации задачи возврата в точку старта результаты по VIO показывают, что визуально‑инерциальные алгоритмы могут обеспечить высокоточную оценку траектории и ориентации БПЛА, однако они, как правило, ориентированы на локальную непрерывную оценку положения, а не на специальную цель прохождения замкнутого маршрута с минимальной ошибкой в конечной точке. Кроме того, полноценные VIO‑системы при высокой точности могут быть избыточно ресурсоёмкими для лёгких бортовых вычислителей малых БПЛА.
|
||||
28
dissertation/chapter_1/chapter_1_5.md
Normal file
28
dissertation/chapter_1/chapter_1_5.md
Normal file
@@ -0,0 +1,28 @@
|
||||
1.5 Корреляционные методы навигации по видовой информации
|
||||
|
||||
Корреляционно‑экстремальные навигационные системы используют сопоставление текущих изображений с эталонной картой или 3D‑моделью местности для коррекции дрейфа ИНС и решения задач типа «вернуться в уже посещённую область». В ряде современных работ эталонная карта строится по данным оптико‑электронной системы в процессе предварительного облёта, после чего в полёте БПЛА периодически сравнивает текущий вид с фрагментами этой карты и уточняет свои координаты.
|
||||
В исследовании Беляева и Зикратова имитируется ситуация, когда эталонная карта и искомая область получены из разных источников (например, спутниковые снимки и бортовая видеокамера) и существенно различаются по текстуре и освещённости [2]. Для таких условий сравниваются:
|
||||
автокорреляционная функция;
|
||||
коэффициент корреляции Пирсона;
|
||||
индекс структурного сходства SSIM;
|
||||
простая нейронная сеть‑перцептрон.
|
||||
Используются различные стратегии обхода карты (прямой проход, проход с перекрытием, по спирали, случайный), а метриками служат время полного обхода для заданного размера окна и успешность нахождения целевой области на зашумлённых и искажённых примерах. Вывод работы состоит в том, что автокорреляционный подход при правильном выборе шага обхода и размера окна позволяет удовлетворить ограничения по времени, оставаясь при этом достаточно точным и устойчивым к различным искажениям, что делает его хорошей основой для визуальной коррекции курса при возврате.
|
||||
В серии работ по оптико‑электронным ыкорреляционно‑экстремальным системам малого БПЛА также показывается, что при разумных ограничениях на высоту, скорость и частоту съёмки возможно достигать приемлемой точности определения координат по цифровой модели местности, построенной заранее или в ходе облёта. Такие системы непосредственно решают задачу поддержания заданного маршрута и коррекции навигации, включая фазу возврата в исходную точку, и могут работать как в полностью автономном, так и в полуавтономном режимах.
|
||||
Особое место занимают работы, где целью явно объявляется возврат БПЛА в точку старта по данным бортовой видеокамеры. В одной из таких статей маршрут от старта до момента потери спутниковой навигации используется для построения глобальной карты в системе географических координат, после чего возврат выполняется за счёт локализации по ранее построенной карте без её дальнейшего расширения; в экспериментах на квадрокоптере «DJI Phantom 3 Pro» авторы демонстрируют устойчивое возвращение в окрестность исходной точки. В другой работе предлагается сопоставление текущих кадров при полёте «домой» с кадрами, ранее записанными при полёте «туда», с использованием меры сходства по яркостным и текстурным признакам, что позволяет оценивать смещение вдоль маршрута и корректировать курс даже без явной 3D‑карты.
|
||||
Эти примеры показывают, что корреляционно‑экстремальные методы и сопоставление видеопоследовательностей дают практически применимые решения задачи возврата в точку старта, которые могут быть реализованы на коммерческих БПЛА при относительно умеренных требованиях к вычислительным ресурсам.
|
||||
Альтернативой сложным VIO‑алгоритмам для малых БПЛА являются корреляционно‑экстремальные методы, сопоставляющие текущие изображения местности с эталонной цифровой картой или ранее накопленной видеопоследовательностью. Такие методы особенно удобны в сценариях, когда доступна карта маршрута или имеется возможность сформировать эталонную базу изображений на этапе прямого полёта (предварительный облёт, съёмка при наличии GPS и т.п.).
|
||||
В простейшем случае эталонная карта представляется в виде двумерного массива яркостей I_{\mathrm{ref}}(x,y), а текущий кадр бортовой камеры — как фрагмент I_{\mathrm{cur}}(u,v), соответствующий некоторой неизвестной области карты. Задача навигации сводится к поиску таких сдвигов (\Delta x,\Delta y), при которых мера сходства между I_{\mathrm{cur}} и соответствующим окном карты I_{\mathrm{ref}}(x+\Delta x,y+\Delta y) максимальна.
|
||||
В работе, посвящённой исследованию автономной навигации БПЛА на основе корреляционных методов сравнения изображений, рассматривается задача поиска области соответствия между двумя изображениями одного и того же участка местности, полученными из разных источников и отличающимися по шуму, освещённости и деталям. В качестве эталона используется «карта» местности, а в качестве текущего наблюдения — искомая область, которая может быть зашумлена, затемнена или искажена; различие источников моделирует эффект смены домена между, например, спутниковыми и бортовыми изображениями.
|
||||
Рассматриваются несколько базовых метрик сходства. Для двух окон изображений X={x_i} и Y={y_i} (последовательно взятые пиксели) автокорреляционная функция и коэффициент корреляции Пирсона записываются как
|
||||
R_{XY}=\sum_{i}\hairsp(x_i-x)(yi-y),ρXY=i (xi-x)(yi-y)i (xi-x)2 i (yi-y)2,
|
||||
где x,y — средние значения яркости в окнах. Индекс структурного сходства SSIM оценивает сходство по яркости, контрасту и структуре и обычно записывается в виде
|
||||
SSIM(X,Y)=\frac{(2\mu_X\mu_Y+C_1)(2\sigma_{XY}+C_2)}{(\mu_X^2+\mu_Y^2+C_1)(\sigma_X^2+\sigma_Y^2+C_2)},
|
||||
где \mu_X,\mu_Y — средние значения, \sigma_X^2,\sigma_Y^2 — дисперсии, \sigma_{XY} — ковариация, C_1,C_2 – малые постоянные для стабилизации.
|
||||
В указанной работе систематически сравниваются автокорреляционная функция, коэффициент корреляции Пирсона, индекс структурного сходства SSIM и простая нейросетевая модель‑перцептрон, применяемые в режиме «скользящего окна» по эталонной карте. Используются различные стратегии обхода окна (прямой проход, проход с перекрытием, по спирали, случайный), а метриками служат время полного обхода для заданного размера окна и успешность нахождения целевой области, в том числе для зашумлённых и искажённых фрагментов карты. Полученные результаты показывают, что автокорреляционный подход при правильно выбранных параметрах окна обеспечивает наилучший компромисс между вычислительной сложностью и точностью: он демонстрирует высокую скорость обработки полного изображения и надёжно выделяет область соответствия как для оригинального, так и для искажённых фрагментов. Методы Пирсона и SSIM уступают по быстродействию при близкой или худшей точности, а нейросетевой перцептрон даёт заметно большую вычислительную нагрузку при ограниченном выигрыше по качеству.
|
||||
Отдельный класс работ посвящён оптико‑электронным корреляционно‑экстремальным навигационным системам малого БПЛА. В таких системах по последовательности оптических изображений строится трёхмерная цифровая модель местности, после чего в полёте выполняется корреляционная обработка текущих и эталонных 3D‑моделей для коррекции инерциальной навигации. На основе эталонной модели M_{\mathrm{ref}} и текущих наблюдений M_{\mathrm{cur}} формируется функционал вида
|
||||
J(p)=-\ Corr(M_{\mathrm{ref}},T(p)M_{\mathrm{cur}}),
|
||||
где p описывает положение и ориентацию БПЛА, T(p) — оператор преобразования модели, а Corr — выбранная корреляционная метрика. Минимизация J(p) позволяет скорректировать оценку состояния инерциальной навигационной системы. В подобных работах подробно исследуется влияние высоты и скорости полёта, перекрытия кадров и периодичности коррекции ИНС на итоговую точность определения координат по эталонной 3D‑модели.
|
||||
Корреляционно‑экстремальные навигационные системы в целом используют сопоставление текущих изображений с эталонной картой или 3D‑моделью местности для компенсации дрейфа ИНС и решения задач типа «вернуться в уже посещённую область». В ряде современных работ эталонная карта строится по данным оптико‑электронной системы в процессе предварительного облёта, после чего в полёте БПЛА периодически сравнивает текущий вид с фрагментами этой карты и уточняет свои координаты. При разумных ограничениях на высоту, скорость и частоту съёмки удаётся достигать приемлемой точности привязки для поддержания маршрута и коррекции навигации, включая фазу возврата в исходную точку, причём системы могут работать как в полностью автономном, так и в полуавтономном режиме.
|
||||
Особое место занимают работы, где целью является возврат БПЛА в точку старта по данным бортовой видеокамеры. В одном из подходов маршрут от старта до момента потери спутниковой навигации используется для построения глобальной карты в системе географических координат, после чего на этапе возврата карта «замораживается», и полёт осуществляется за счёт локализации по ранее построенной карте без её дальнейшего расширения. Локализация сводится к сопоставлению текущих кадров с ближайшими по траектории эталонными фрагментами и оценке положения аппарата относительно координатной системы карты; по этим оценкам формируются управляющие воздействия для следования по маршруту в обратном направлении. Эксперименты, в том числе на квадрокоптере «DJI Phantom 3 Pro», показывают возможность устойчивого возвращения в окрестность исходной точки при отсутствии спутниковой навигации.
|
||||
В другом подходе предлагается сопоставление текущих кадров при полёте «домой» с кадрами, ранее записанными при полёте «туда», с использованием меры сходства по яркостным и текстурным признакам. По максимальному значению меры сходства определяется индекс наиболее похожего кадра в эталонной последовательности, что фактически задаёт положение БПЛА вдоль маршрута. Это позволяет оценивать смещение вдоль трассы и корректировать курс даже без явной трёхмерной модели сцены.
|
||||
Таким образом, корреляционно‑экстремальные методы и сопоставление видеопоследовательностей дают практически применимые решения задачи возврата в точку старта, которые могут быть реализованы на коммерческих БПЛА при относительно умеренных требованиях к вычислительным ресурсам. Для задачи возврата в точку старта они особенно привлекательны тем, что позволяют сопоставлять текущий кадр с локальными фрагментами эталонной карты вдоль маршрута без построения полной плотной карты сцены и без сложной фактор‑графовой оптимизации, что делает такие подходы перспективными для малогабаритных БПЛА с ограниченными ресурсами.
|
||||
9
dissertation/chapter_1/chapter_1_6.md
Normal file
9
dissertation/chapter_1/chapter_1_6.md
Normal file
@@ -0,0 +1,9 @@
|
||||
1.6 Алгоритмы возврата в точку старта по видеопоследовательностям
|
||||
|
||||
Ряд работ посвящён непосредственно задаче возврата БПЛА в начальную точку полёта по данным бортовой видеокамеры без использования GPS/ГЛОНАСС. В этих подходах используется либо видеопоследовательность, заранее записанная по маршруту, либо изображения, накопленные в ходе прямого полёта до потери спутниковых сигналов.
|
||||
В статье, посвящённой визуальной навигации автономно летящего БПЛА с целью его возвращения в точку старта, предлагается алгоритм, сочетающий идеи визуальной одометрии и SLAM при явном разделении этапов картографирования и локализации. На участке от старта до потери сигналов GPS/ГЛОНАСС бортовая система по данным видеокамеры и актуальным навигационным данным строит карту местности в виде множества 3D‑точек‑признаков с известными географическими координатами.
|
||||
После потери спутниковой навигации запускается миссия возврата: текущее положение БПЛА оценивается только относительно ранее построенной карты, без дальнейшего её расширения, что уменьшает вычислительные затраты и предотвращает неконтролируемый рост ошибок. Локализация заключается в сопоставлении признаков, выделенных на текущем кадре, с ближайшими по траектории участками карты и в последующей оценке положения и ориентации аппарата относительно системы координат карты; по этой оценке, формируются управляющие воздействия для следования по маршруту в обратном направлении.
|
||||
Преимуществом такого подхода является отсутствие накопления ошибок, характерного для чистой визуальной одометрии и инерциальной навигации: поскольку локализация ведётся относительно фиксированной карты, ошибка не растёт без ограничений с длиной пройденного пути. Алгоритм реализован и протестирован на квадрокоптере DJI Phantom 3 Pro; по результатам экспериментов обеспечивается надёжное возвращение БПЛА в окрестность точки старта при отсутствии спутниковой навигации.
|
||||
В другой работе рассматривается навигация БЛА с помощью бортовой видеокамеры, где также решается задача возврата аппарата в начальную точку полёта в автономном режиме. Предлагаемый алгоритм сопоставляет каждый текущий кадр, полученный при полёте «домой», с кадрами, записанными при полёте от исходной точки к цели, что позволяет по максимальному значению меры сходства оценивать смещение вдоль маршрута и корректировать курс.
|
||||
Если заранее доступна эталонная видеопоследовательность или последовательность изображений (например, спутниковых снимков), покрывающая весь маршрут от исходной точки до точки назначения, описанный подход обеспечивает как полёт к цели, так и возврат, полностью опираясь на видовую информацию. В работе приводятся результаты компьютерного моделирования и экспериментов с использованием видеозаписей с бортовой камеры квадрокоптера «DJI Phantom», демонстрирующие возможность реализации подобного алгоритма на реальной технике.
|
||||
Отмеченные алгоритмы являются прямыми примерами решения задачи возврата в точку старта, близкой к рассматриваемой в диссертации постановке. Они показывают, что раздельное ведение картографирования и локализации, а также сопоставление текущих кадров с ранее записанными, позволяют обеспечить устойчивый возврат при умеренной вычислительной сложности.
|
||||
11
dissertation/chapter_1/chapter_1_7.md
Normal file
11
dissertation/chapter_1/chapter_1_7.md
Normal file
@@ -0,0 +1,11 @@
|
||||
1.7 Интеграция классических и нейросетевых методов для задачи работы
|
||||
|
||||
С появлением мощных методов глубокого обучения задачи сопоставления изображений для навигации дополнились новыми инструментами, которые особенно актуальны при сильном рассогласовании между эталонной картой и текущими наблюдениями (domain shift).
|
||||
Генеративно‑состязательные сети (GAN) позволяют трансформировать изображения из разных доменов (например, спутниковые и аэрофотоснимки с борта) к единому стилю, что облегчает последующее корреляционное сопоставление.
|
||||
Сиамские сети для визуальной одометрии (SiaN‑VO) используют две ветви CNN для извлечения признаков из пары кадров и полносвязные слои для оценки либо скалярной меры сходства, либо параметров гомографии/относительного движения.
|
||||
Трансформер‑модели для VO (DeepVO, VoT и др.) за счёт механизма внимания улучшают устойчивость к дрейфу и позволяют лучше учитывать долгосрочные зависимости в видеопотоке, однако требуют существенно больших вычислительных ресурсов, вплоть до использования современных GPU‑ускорителей.
|
||||
В контексте разрабатываемого алгоритма возврата в точку старта такие модели могут быть использованы:
|
||||
на этапе нормализации изображений (GAN) для уменьшения влияния различий по освещённости, сезону и типу съёмки;
|
||||
на этапе оценки степени совпадения текущего кадра с эталонными фрагментами карты (сиамская сеть, выдающая значение сходства в диапазоне от 0 до 1);
|
||||
при вычислении матрицы гомографии между кадрами, что позволяет точнее оценивать относительное смещение и поворот БПЛА при сопоставлении с эталонами.
|
||||
Тем самым создаётся гибридный подход, сочетающий проверенные корреляционные методы (автокорреляция, sliding‑window‑обход и т.п.) с нейросетевыми оценщиками сходства и гомографии. Классические методы обеспечивают гарантированное быстродействие и предсказуемое поведение на борту малых БПЛА, тогда как нейросетевые компоненты повышают устойчивость к сложным условиям съёмки и рассогласованию карт, что особенно важно для устойчивого возврата в точку старта в реальных условиях эксплуатации.
|
||||
25
dissertation/chapter_1/chapter_1_8.md
Normal file
25
dissertation/chapter_1/chapter_1_8.md
Normal file
@@ -0,0 +1,25 @@
|
||||
1.8 Метрики оценки качества навигации и возврата
|
||||
|
||||
Визуально‑инерциальные алгоритмы традиционно оцениваются с использованием наборов данных EuRoC, TUM‑VI, KITTI и др., где для каждого датасета известна «истинная» траектория. Наиболее распространённые метрики:
|
||||
ATE (Absolute Trajectory Error) – среднеквадратическая ошибка по положению между восстановленной и эталонной траекториями, измеряемая по всей длине маршрута; часто используется как интегральный показатель качества VIO‑алгоритма;
|
||||
RPE (Relative Pose Error) – ошибка относительного смещения и поворота на фиксированном временном горизонте, характеризующая локальную стабильность и дрейф [19];
|
||||
процентная ошибка по расстоянию – отношение ошибки конечного положения к длине траектории, выраженное в процентах; для ряда VIO‑решений она составляет доли процента даже на километровых дистанциях.
|
||||
ATE измеряет насколько далеко в среднем лежит оцененная траектория от истинной после выравнивания. Для каждого момента времени i есть истинные позы БПЛА T_i^{gt} (ground truth) и оцененные позы T_i^{est} (estimated). Определим матрицу S, преобразовывает каждую оцененную позу таким образом, что суммарная разница между позами минимизируется. Для каждого момента времени определим ошибку
|
||||
E_i=\left(T_i^{gt}\right)^{-1}ST_i^{est}
|
||||
Итоговая метрика определяется следующим образом:
|
||||
ATE_{RMSE}=\sqrt{\frac{1}{N}\sum_{i}\left|\left|trans\left(E_i\right)\right|\right|^2},
|
||||
где trans\left(E_i\right) – норма трансляции, иными словами – смещение, а N – количество кадров. Для интуитивного понимания можно представить, что ATE характеризует насколько в среднем расходится оцененная траектория полета от истинной в каждый момент времени.
|
||||
RPE измеряет локальную ошибку относительного движения на фиксированном интервале (между кадрами). Для этого в каждый момент времени i рассчитывается относительное движение истинной траектории:
|
||||
\Delta T_i^{gt}=\left(T_i^{gt}\right)^{-1}T_{i+\Delta}^{gt}
|
||||
И похожим образом для оцененной траектории:
|
||||
\Delta T_i^{est}=\left(T_i^{est}\right)^{-1}T_{i+\Delta}^{est}
|
||||
Теперь, разница между различными смещен рассчитывается по следующей формуле:
|
||||
E_i^{rel}=\left(\Delta T_i^{gt}\right)^{-1}\Delta T_i^{est}
|
||||
Аналогичным образом высчитывается средняя ошибка на каждый переход кадров. Преимущество RPE в том, что он не зависит от начального смещения и ориентации и лучше показывает накопление ошибок со временем.
|
||||
Данные метрики применяются в различных публикациях. Например, в работе по SP‑VIO показано, что алгоритм обеспечивает более низкий ATE по сравнению с VINS‑Mono и OpenVINS как на общеизвестных датасетах (EuRoC, TUM‑VI, KITTI), так и на собственных данных, при этом сохраняется вычислительная эффективность, характерная для фильтрационных подходов. В экспериментах с автомобильной траекторией длиной порядка 4.5 км достигнута ошибка порядка долей процента, что критично для задач возврата: подобная точность позволяет вернуться в небольшую окрестность исходной точки без внешней навигации.
|
||||
Для аппаратного ускорителя «Navion» оцениваются как точность траектории, так и энергетическая эффективность; средняя ошибка траектории составляет около 0.28% на сложном мультироторном датасете EuRoC, при этом потребляемая мощность — всего порядка 24 мВт при частоте обработки до 171 кадр/с. Такие характеристики демонстрируют, что при соответствующей аппаратной поддержке VIO может выступать в качестве базового навигационного ядра и для задачи возврата [15].
|
||||
Для корреляционно‑экстремальных систем вводятся метрики:
|
||||
максимальная ошибка привязки искомой области к карте (в пикселях карты или в метрах на местности);
|
||||
вероятность правильного обнаружения области при зашумлении, затемнении и искажениях (моделируется как имитация «domain shift» между разными источниками изображений);
|
||||
время обхода карты при различных стратегиях сканирования (прямой обход, обход с перекрытием, по спирали, случайный), определяющее возможность работы в реальном времени.
|
||||
В исследовании Беляева и Зикратова проводится количественное сравнение нескольких корреляционных метрик (автокорреляция, коэффициент Пирсона, SSIM, однослойный перцептрон) по точности обнаружения и времени обработки при различных режимах обхода скользящего окна [3]. Показано, что автокорреляционный подход обеспечивает оптимальное сочетание точности и быстродействия, а также устойчив к зашумлению и искажению эталонной карты.
|
||||
7
dissertation/chapter_1/chapter_1_9.md
Normal file
7
dissertation/chapter_1/chapter_1_9.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## 1.9 Сопоставление подходов и место данной работы
|
||||
|
||||
Рассмотренные подходы можно условно разделить на три группы: высокоточные VIO‑алгоритмы с плотной или разреженной картой («Navion», SP‑VIO и др.), корреляционно‑экстремальные методы навигации по эталонным картам и специализированные алгоритмы возврата в точку старта по видеопоследовательностям.
|
||||
VIO‑системы обеспечивают высокую точность оценки траектории и могут работать в широком диапазоне условий, но их реализация на малых БПЛА требует либо специализированных аппаратных ускорителей (как в «Navion»), либо достаточно производительных процессоров, а также тщательной настройки фильтрационных или оптимизационных процедур. Корреляционные методы и корреляционно‑экстремальные навигационные системы менее универсальны, но гораздо проще по вычислительной схеме, что делает их привлекательными для ресурсно‑ограниченных платформ.
|
||||
Алгоритмы возврата в точку старта по видеопоследовательностям, предложенные в работах Жука и др., а также Залесского и Шувалова, демонстрируют, что задача возврата может быть решена без сложной глобальной оптимизации и построения полной карты, если на этапе прямого полёта или заранее сформирована достаточная база эталонных кадров [4,5]. При этом ряд практических вопросов остаётся открытым: влияние рассогласования между эталонной и текущей картой (domain shift), устойчивость к изменениям освещённости и сезона, а также выбор метрик сходства и стратегий обхода карт, обеспечивающих минимальную погрешность конечных координат при ограниченном времени обработки.
|
||||
В данной работе предполагается развить идеи корреляционно‑экстремальной навигации и алгоритмов возврата в точку старта, дополнив их современными методами глубокого обучения для сопоставления изображений. В частности, рассматривается использование генеративно‑состязательных сетей (GAN) для приведения изображений из разных доменов к единому стилю, сиамских нейросетей для оценки степени сходства текущего кадра с эталонными участками карты и для оценки матрицы гомографии между кадрами.
|
||||
Таким образом, обзор показывает, что существующие решения либо обеспечивают высокую точность ценой значительных ресурсных затрат (VIO), либо ориентированы на специальные условия и не учитывают в полной мере проблему рассогласования карт и необходимость интеграции с методами глубокого обучения для обработки изображений. Это определяет актуальность разработки алгоритма навигации возврата в точку старта, сочетающего достоинства корреляционных подходов и современных методов глубокого обучения при учёте ограничений бортовой вычислительной платформы малых БПЛА.
|
||||
15
dissertation/chapter_1/readme.md
Normal file
15
dissertation/chapter_1/readme.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Глава 1. Аналитический обзор современного состояния проблемы
|
||||
|
||||
## Содержание
|
||||
|
||||
| Раздел | Название | Файл |
|
||||
|--------|----------|------|
|
||||
| 1.1 | Задача возврата в точку старта в системах навигации БПЛА | chapter_1_1.md |
|
||||
| 1.2 | Классификация методов навигации БПЛА | chapter_1_2.md |
|
||||
| 1.3 | Интегрированные инерциально‑спутниковые навигационные системы | chapter_1_3.md |
|
||||
| 1.4 | Визуальная и визуально-инерциальная одометрия для автономной навигации | chapter_1_4.md |
|
||||
| 1.5 | Корреляционные методы навигации по видовой информации | chapter_1_5.md |
|
||||
| 1.6 | Алгоритмы возврата в точку старта по видеопоследовательностям | chapter_1_6.md |
|
||||
| 1.7 | Интеграция классических и нейросетевых методов для задачи работы | chapter_1_7.md |
|
||||
| 1.8 | Метрики оценки качества навигации и возврата | chapter_1_8.md |
|
||||
| 1.9 | Сопоставление подходов и место данной работы | chapter_1_9.md |
|
||||
2
dissertation/chapter_1/todo.md
Normal file
2
dissertation/chapter_1/todo.md
Normal file
@@ -0,0 +1,2 @@
|
||||
- [x] Оглавление в readme.md оформить в виде таблицы
|
||||
- [x] распарсить _temp_full_content.md по файлам
|
||||
@@ -0,0 +1,19 @@
|
||||
2.1 Постановка задачи возврата БПЛА в точку старта
|
||||
|
||||
В данной работе рассматривается задача автономного возврата беспилотного летательного аппарата в точку старта при отсутствии или потере сигналов спутниковой навигации (GPS/ГЛОНАСС) и любой другой внешней связи. Дрон оснащен:
|
||||
бортовой камерой для получения изображений местности в реальном времени;
|
||||
инерциальной навигационной системой, обеспечивающей измерения угловых скоростей и ускорений;
|
||||
бортовым вычислителем, способным выполнять обработку изображений и управляющие вычисления.
|
||||
В рамках настоящей работы предполагается только наличие бортовой камеры.
|
||||
Исходные данные:
|
||||
Циклический маршрут – последовательность координат точек \{p_0,p_1,\ldots,p_n\}, где p_0=p_n (первая точка совпадает с последней);
|
||||
Эталонная карта местности – изображение или последовательность изображений вдоль траектории маршрута, полученная до начала полета (например, спутниковые снимки или предварительная загрузка участков карты);
|
||||
Видеопоток в реальном времени – последовательности кадров, получаемых бортовой камерой в процессе полета.
|
||||
Задача: разработать алгоритм навигации, который корректирует угол и направление полета таким образом, чтобы пройти через каждую заданную точку маршрута \{p_1,p_2,\ldots,p_{n-1}\}, вернуться в исходную позицию p_o с наименьшей с наименьшей погрешностью в координатах и обеспечить устойчивость к различиям между эталонной картой и реальными изображениями.
|
||||
Формально говоря, цель состоит в минимизации конечной ошибки позиционирования:
|
||||
e_{final}=\left|\left|p_{actual}\left(T\right)-p_0\right|\right|,
|
||||
Где p_{actual}\left(T\right) – фактическое положение БПЛА в момент завершения возврата, p_0 – исходная точка старта.
|
||||
Дополнительным критерием точности может выступить минимизация средней накапливаемой ошибки на каждой точке:
|
||||
s_{final}=\sum_{i=1}^{P}\left|\left|p_{actual}\left(i\right)-p_i\right|\right|,
|
||||
где p_{actual}\left(i\right) – фактическое положение БПЛА при прохождении через точку i, а p_i – положение точки i, P - количество точек
|
||||
|
||||
5
dissertation/chapter_2/2.1_formalization/readme.md
Normal file
5
dissertation/chapter_2/2.1_formalization/readme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 2.1 Формализация задачи возврата БПЛА в точку старта
|
||||
|
||||
## Содержание раздела
|
||||
|
||||
Описание формализации задачи находится в файле 2.1_formalization.md
|
||||
@@ -0,0 +1,39 @@
|
||||
2.2 Базовое решение задачи
|
||||
|
||||
Обозначим последовательность изображений \{u_i\ |\ i\in[0..N]}, где N – итоговое количество кадров, которое было получено во время всего полета. Пусть
|
||||
I_i\left(x,y\right) – интенсивность пикселя в позиции (y,\ x), где x\ \in[0..W-1] и y\ \in[0..W-1], W – ширина кадра.
|
||||
Матрица H_i – матрица гомографии, которая описывает переход от кадра u_{i-1} к кадру u_i:
|
||||
I_{i-1}\left(x,y\right)=I_i\left(x^\prime,y^\prime\right),
|
||||
t\ast\left(\begin{matrix}x\prime\\y\prime\\1\\\end{matrix}\right)=H_i\times\left(\begin{matrix}x\\y\\1\\\end{matrix}\right)
|
||||
Матрица гомографии (H) описывает проективное между двумя плоскостями и может быть представлена в следующем виде:
|
||||
H=K_1\times R\times T\times{K_2}^{-1},
|
||||
где K_1 и K_2 – матрицы внутренних параметров камеры, R – матрица поворотов, T – матрица трансляции.
|
||||
Матрицы K_1 и K_2 представляют собой матрицы внутренних параметров и обычно равны между собой (различаются центры изображений). Однако в случаях разного разрешения картинок они могут иметь разные параметры. В общем виде такая матрица представляется следующим образом:
|
||||
|
||||
K_i=\left(\begin{matrix}f&\gamma&c_x^i\\0&f&c_y^i\\0&0&1\\\end{matrix}\right),
|
||||
|
||||
Где f – фокусное расстояние в пикселях, c_x и c_y – координаты оптического центра камеры на плоскости изображения, \gamma – коэффициент скоса, описывающий угол наклона пикселей (в рамках симуляции равен 0).
|
||||
Матрицу R можно получить при помощи стандартной функции OpenCV decomposeHomographyMat, также эту матрицу можно вычислить самостоятельно, поскольку первые два столбца матрицы гомографии H инвариантны смещению.
|
||||
Матрица R представляет собой следующий вид:
|
||||
R=R_x\times R_y\times R_z
|
||||
Где R_x,R_y,R_z – матрицы вращения вокруг осей OX,\ OY,\ OZ соответственно.
|
||||
Если известны угол рыскания \psi, тангажа \theta и крена \gamma, то эти матрицы вычисляются следующим образом:
|
||||
|
||||
R_x\left(\gamma\right)=\left(\begin{matrix}1&0&0\\0&cos\left(\gamma\right)&-sin\left(\gamma\right)\\0&sin\left(\gamma\right)&cos\left(\gamma\right)\\\end{matrix}\right),
|
||||
|
||||
R_y\left(\theta\right)=\left(\begin{matrix}cos\left(\theta\right)&0&sin\left(\theta\right)\\0&1&0\\-sin\left(\theta\right)&0&cos\left(\theta\right)\\\end{matrix}\right),
|
||||
|
||||
R_z\left(\psi\right)=\left(\begin{matrix}cos\left(\psi\right)&-sin\left(\psi\right)&0\\sin\left(\psi\right)&cos\left(\psi\right)&0\\0&0&1\\\end{matrix}\right),
|
||||
|
||||
Зная матрицу H,\ K_1, K_2,\ и R, можно получить матрицу T:
|
||||
|
||||
T=R^{-1}\times K_1^{-1}\times H\times K_2
|
||||
л.в.что:T=R-1×K1-1×K1×R×T×K2-1×K2
|
||||
|
||||
Матрица трансляции имеет вид:
|
||||
|
||||
T=\lambda0λxf0λλyf00λz=λ *10xf01yf00z
|
||||
|
||||
Таким образом, можно получить смещение между кадрами, зная матрицы внутренних параметров камеры и матрицу гомографии.
|
||||
Чтобы получить положение БПЛА в момент времени t, достаточно получить матрицу трансляции из произведения всех промежуточных матриц гомографии (обозначим за G_t) на префиксе кадров [0..t]:
|
||||
G_t=\prod_{i=1}^{t}H_i=H_1\times H_2\times\ldots\times H_t
|
||||
5
dissertation/chapter_2/2.2_base_solution/readme.md
Normal file
5
dissertation/chapter_2/2.2_base_solution/readme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 2.2 Базовое решение задачи
|
||||
|
||||
## Содержание раздела
|
||||
|
||||
Описание базового решения находится в файле 2.2_base_solution.md
|
||||
@@ -0,0 +1,51 @@
|
||||
2.3.1 Применение архитектуры сиамских близнецов для сопоставления кадров из различных доменов
|
||||
|
||||
Абстрактно, сиамские близнецы представляют собой следующую архитектуру (рисунок 1):
|
||||
На вход подаются два или более объекта с одинаковой сигнатурой.
|
||||
Каждая входная информация проходит одинаковый пайплайн обработки (получение признаков при помощи CNN).
|
||||
Полученные наборы признаков объединяются в вектор (1\times M).
|
||||
Голова модели представляет собой полносвязную нейронную сеть.
|
||||
|
||||
|
||||
|
||||
Рисунок 1 – Абстрактная архитектура сиамских близнецов
|
||||
|
||||
В качестве входной информации выступят два кадра: текущий снимок видеокамеры и эталонный снимок, для которого точно известна позиция. Выходная информация будет представлять собой вещественное число от 0 до 1 – степень схожести двух картинок (Рисунок 2).
|
||||
|
||||
|
||||
|
||||
Рисунок 2 – Интерфейс алгоритма (входные и выходные данные)
|
||||
|
||||
На базе такой архитектуры реализована модель «SiaN-Similarity» с использованием фреймворка PyTorch и включает общий скелет на базе предобученной модели «ResNet18» и голову сравнения (рисунок 3). На вход подаются два изображения размером \left(B,3,256,256\right), где B – размер пакета данных (batch). Эти изображения представляют собой текущий кадр («Image 1») и кандидат-эталон «Image 2». Извлеченные при помощи «ResNet18» признаки f_1 и f_2 объединяются в вектор [f_1,f_2,f1–f2,f1*f2] размером \left(B,2048\right), так как каждый из векторов f_1 и f_2 имеет размер (B,\ 256). Все это проходит через многослойный перцептрон (MLP), где выход – вероятность схожести картинок (рисунок 3).
|
||||
Обучение модели проводилось на собственном датасете «ya_go_maps» - собственный набор, состоящий из 327 пар изображений разных доменов (261 тренировочных и 66 валидационных) с бинарными метками (одинаковы или разный домен). Датасет сформирован на основе снимков из различных онлайн-карт («Яндекс.Карты» и «Google Maps»). Для обучения использовалась функция потерь Binary Cross Entropy Loss (BCELoss), которая идеально подходит для бинарной классификации схожести изображений:
|
||||
l_n=-\left(y_nlog\left(x_n\right)+\left(1-y_n\right)log\left(1-x_n\right)\right),
|
||||
где x_n – предсказанная вероятность, y_n – истинная метка (1 для одинаковых пар снимков, 0 – для разных). Средняя потеря по батчу вычисляется как
|
||||
\frac{1}{N}\sum_{n}l_n
|
||||
BCELoss выбрана из-за совместимости с сигмоидным выходом и способности эффективно различать градиенты для вероятностей, что отлично подходит для задач сопоставления изображений с доменным сдвигом.
|
||||
|
||||
|
||||
Рисунок 3 – Архитектура модели «SiaN-Similarity»
|
||||
|
||||
Платформа обучения – «Kaggle» с видеокартой «Tesla P100». Весь процесс обучения занял 24 минуты и остановился на 56 эпохе (ранняя остановка). Train Loss снизилась до 0.27 и монотонно убывала на протяжении всех эпох (за небольшими исключениями. После ~30 эпохи модель начала переобучаться и сработала ранняя остановка (early stopping). Лучшая Val Loss – 0.2929 (эпоха 36) при Val Accuracy 0.90. Однако лучшая точность достигнута на последней эпохе – 0.9630, правда функция потерь в том случае слишком высокая и слишком низкий recall, что критично для дальних полетов, ведь высок риск еще большего накопления ошибок.
|
||||
|
||||
|
||||
|
||||
Рисунок 4 – Графики обучения «SiaN-Similiarity»
|
||||
|
||||
Модель демонстрирует консервативный характер: высокий recall минимизирует пропуски истинных совпадений эталонных кадров (false negatives), что критично для коррекции траектории БПЛА. Низкая объясняется доменным сдвигом и малым размером датасета – модель склонна классифицировать неоднозначные пары как одинаковые. Это приемлемо для навигации, где false positives приводят лишь к проверке дополнительных кандидатов, а false negatives могут вызвать потерю позиции (рисунок 5).
|
||||
|
||||
|
||||
|
||||
Рисунок 5 – Матрица ошибок «SiaN-Similarity»
|
||||
|
||||
|
||||
На рисунке 6 и 7 показаны примеры верных предсказании модели и неверных соответственно.
|
||||
|
||||
|
||||
|
||||
Рисунок 6 – Верно угаданные сэмплы «SiaN-Similarity»
|
||||
|
||||
|
||||
|
||||
Рисунок 7 – Неверно угаданные сэмплы «SiaN-Similiarity»
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
2.3.2 Применение архитектуры сиамских близнецов для вычисления матрицы гомографии между двумя кадрами
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
2.3.3 Применение архитектуры сиамских близнецов для вычисления матрицы гомографии между двумя кадрами
|
||||
|
||||
|
||||
5
dissertation/chapter_2/2.3_deep_learning/2.3_overview.md
Normal file
5
dissertation/chapter_2/2.3_deep_learning/2.3_overview.md
Normal file
@@ -0,0 +1,5 @@
|
||||
2.3 Методы глубого обучения
|
||||
|
||||
Основным минусом базового решения является то, что любая его реализация предполагает накопление погрешности. В таком случае корректировка позиции может стать хорошим дополнением для снижения дрейфа.
|
||||
Одной из проблем такого подхода является поиск необходимого эталонного снимка. Снимки могут совершаться в разное время суток, в разных годах, при неодинаковых условиях (ветер, туман, погода, сезон года), но иметь какие-то общие схожие паттерны. Классические способы решения сопоставления таких кадров работают крайне плохо, однако модели на основе нейронных сетей справляются с этой задачей куда лучше.
|
||||
|
||||
8
dissertation/chapter_2/2.3_deep_learning/readme.md
Normal file
8
dissertation/chapter_2/2.3_deep_learning/readme.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# 2.3 Эталонные снимки
|
||||
|
||||
## Содержание раздела
|
||||
|
||||
| Подраздел | Название | Файл |
|
||||
|-----------|----------|------|
|
||||
| 2.3.1 | Применение архитектуры сиамских близнецов для сопоставления кадров из различных доменов | 2.3.1_siamese_match.md |
|
||||
| 2.3.2 | Применение архитектуры сиамских близнецов для вычисления матрицы гомографии | 2.3.2_siamese_homography.md |
|
||||
51
dissertation/chapter_2/2.4_dataset/2.4_dataset.md
Normal file
51
dissertation/chapter_2/2.4_dataset/2.4_dataset.md
Normal file
@@ -0,0 +1,51 @@
|
||||
2.4 Датасет
|
||||
|
||||
Для обучения и тестирования моделей глубокого обучения, описанных в разделе 2.3, был создан специализированный датасет `ya_go_maps`, состоящий из пар изображений, полученных из двух различных картографических источников: Яндекс.Карт и Google Maps.
|
||||
|
||||
\section{Структура датасета}
|
||||
|
||||
Каждый элемент датасета представляет собой пару изображений одной и той же географической локации, полученных с различных картографических сервисов. Изображения в паре имеют одинаковое разрешение и именуются по шаблону: `{id}_google.png` и `{id}_yandex.png`, где `id` — порядковый номер пары. Например, изображения `0063_google.png` и `0063_yandex.png` соответствуют одному и тому же участку местности.
|
||||
|
||||
\section{Географические координаты и масштаб}
|
||||
|
||||
Сбор данных осуществлялся в пределах следующего географического региона:
|
||||
|
||||
\begin{itemize}
|
||||
\item Широта: от 49.134520 до 49.235065
|
||||
\item Долгота: от 55.767660 до 55.825204
|
||||
\end{itemize}
|
||||
|
||||
Данный регион соответствует территории города Казань. Снимки выполнялись на уровне приближения (zoom) 18, что обеспечивает детализацию, достаточную для распознавания характерных объектов на местности.
|
||||
|
||||
\section{Трансформация перспективы}
|
||||
|
||||
При полёте беспилотного летательного аппарата камера направлена приблизивно вниз, что существенно отличается от ракурса, под которым пользователь просматривает карту в веб-браузере. Для корректного обучения модели, которая будет применяться в реальных условиях полёта, к исходным снимкам карт применяется аффинное преобразование перспективы. Данная трансформация выполняется с использованием класса `Simulator` (файл `simulator.py`).
|
||||
|
||||
После применения преобразования изображения масштабируются до размера `CHUNK_WIDTH / 2` пикселей. Значение константы `CHUNK_WIDTH` определяется в файле `constants.py` и соответствует размеру кадра, с которым работает система технического зрения автопилота.
|
||||
|
||||
\section{Формирование датасета}
|
||||
|
||||
Процесс генерации датасета реализован в файле `generate_dataset.py` и включает следующие этапы:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Определение географических координат (широта и долгота) путём случайной выборки или перебора точек в пределах заданного диапазона
|
||||
\item Загрузка снимков с Яндекс.Карт и Google Maps для выбранной локации
|
||||
\item Применение аффинного преобразования перспективы к обоим снимкам
|
||||
\item Масштабирование изображений до целевого разрешения
|
||||
\item Сохранение пары изображений в директорию `dataset_ya_go_maps/`
|
||||
\end{enumerate}
|
||||
|
||||
\section{Применение датасета}
|
||||
|
||||
Датасет `ya_go_maps` используется для обучения сиамских нейронных сетей, предназначенных для решения двух задач:
|
||||
|
||||
\begin{itemize}
|
||||
\item Сопоставление изображений из различных доменов (раздел 2.3.1)
|
||||
\item Вычисление матрицы гомографии между кадрами (раздел 2.3.2)
|
||||
\end{itemize}
|
||||
|
||||
Наличие парных изображений от разных картографических провайдеров позволяет модели научиться находить соответствия между изображениями, полученными в различных условиях визуализации, что критически важно для системы навигации БПЛА.
|
||||
|
||||
\section{Доступность}
|
||||
|
||||
Датасет доступен в двух версиях: `ya_go_maps_v1.zip` (около 463 МБ) и `ya_go_maps_v2.zip` (около 197 МБ). Версия v2 содержит оптимизированный набор данных, полученный путём перебора географических точек по сетке 20×20.
|
||||
5
dissertation/chapter_2/2.4_dataset/readme.md
Normal file
5
dissertation/chapter_2/2.4_dataset/readme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 2.4 Датасет
|
||||
|
||||
## Содержание раздела
|
||||
|
||||
Описание датасета находится в файле 2.4_dataset.md
|
||||
1
dissertation/chapter_2/2.4_dataset/todo.md
Normal file
1
dissertation/chapter_2/2.4_dataset/todo.md
Normal file
@@ -0,0 +1 @@
|
||||
- [x] Описать из datasets\ya_go_maps, как это сделано. Взяты lat и lon периметром 5 км какой-то области города, и образованы таким образом снимки. Написать эту подглаву
|
||||
2
dissertation/chapter_2/2.5_training/2.5_training.md
Normal file
2
dissertation/chapter_2/2.5_training/2.5_training.md
Normal file
@@ -0,0 +1,2 @@
|
||||
2.5 Обучение моделей глубокого обучения
|
||||
|
||||
5
dissertation/chapter_2/2.5_training/readme.md
Normal file
5
dissertation/chapter_2/2.5_training/readme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 2.5 Обучение моделей глубокого обучения
|
||||
|
||||
## Содержание раздела
|
||||
|
||||
Описание обучения моделей находится в файле 2.5_training.md
|
||||
13
dissertation/chapter_2/readme.md
Normal file
13
dissertation/chapter_2/readme.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Глава 2. Постановка задачи и выбор методов решения
|
||||
|
||||
## Содержание
|
||||
|
||||
| Раздел | Название | Путь |
|
||||
|--------|----------|------|
|
||||
| 2.1 | Формализация задачи возврата БПЛА в точку старта | 2.1_formalization/ |
|
||||
| 2.2 | Базовое решение задачи | 2.2_base_solution/ |
|
||||
| 2.3 | Методы глубокого обучения | 2.3_deep_learning/ |
|
||||
| 2.3.1 | Сиамские близнецы для сопоставления кадров из различных доменов | 2.3_deep_learning/2.3.1_siamese_match.md |
|
||||
| 2.3.2 | Сиамские близнецы для вычисления матрицы гомографии | 2.3_deep_learning/2.3.2_siamese_homography.md |
|
||||
| 2.4 | Датасет | 2.4_dataset/ |
|
||||
| 2.5 | Обучение моделей глубокого обучения | 2.5_training/ |
|
||||
2
dissertation/chapter_2/todo.md
Normal file
2
dissertation/chapter_2/todo.md
Normal file
@@ -0,0 +1,2 @@
|
||||
- [x] Оглавление в readme.md оформить в виде таблицы
|
||||
- [x] распарсить _temp_full_content.md по папкам и файлам
|
||||
@@ -0,0 +1,56 @@
|
||||
# 3.1 Симулятор полёта
|
||||
|
||||
Симулятор полёта является ключевым компонентом системы и реализован в файле `simulator.py`. Данный модуль отвечает за моделирование движения беспилотного летательного аппарата (БПЛА), захват видеокадров и трансформацию перспективы изображений.
|
||||
|
||||
\section{Основные функции симулятора}
|
||||
|
||||
Симулятор выполняет следующие функции:
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{Управление движением} — изменение координат БПЛА на основе заданной скорости и угла курса
|
||||
\item \textbf{Захват кадров} — получение скриншотов с картографических источников
|
||||
\item \textbf{Перспективная трансформация} — преобразование изображений для имитации вида с БПЛА
|
||||
\item \textbf{Управление ориентацией} — установка тангажа, крена и масштаба изображения
|
||||
\end{itemize}
|
||||
|
||||
\section{Модель позиционирования}
|
||||
|
||||
Позиция БПЛА описывается шестью параметрами: координаты (x, y, z), угол рыскания (yaw), тангаж (pitch) и крен (roll). Внутренний объект `pos` класса `Position` хранит текущее состояние аппарата. Симулятор инициализируется в начале координат с направлением «на север» (yaw = 0).
|
||||
|
||||
Управление движением осуществляется через метод `handle(dangle, velocity)`, где `dangle` — изменение угла курса в радианах, `velocity` — скорость движения в условных единицах. Смещение вычисляется по формулам:
|
||||
|
||||
\begin{equation}
|
||||
dx = \cos\left(\frac{\pi}{2} + yaw\right) \cdot velocity
|
||||
\end{equation}
|
||||
\begin{equation}
|
||||
dy = \sin\left(\frac{\pi}{2} + yaw\right) \cdot velocity
|
||||
\end{equation}
|
||||
|
||||
\section{Перспективная трансформация}
|
||||
|
||||
При полёте БПЛА камера направлена приблизительно вниз, что существенно отличается от ракурса, под которым пользователь просматривает карту в веб-браузере. Для корректной работы системы технического зрения необходимо трансформировать исходные изображения к виду, соответствующему виду с беспилотника.
|
||||
|
||||
Трансформация выполняется методом `_apply_perspective_transform`, который:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Получает изображение от картографического провайдера
|
||||
\item Вычисляет матрицу гомографии на основе текущей позиции
|
||||
\item Применяет аффинное преобразование перспективы с помощью OpenCV
|
||||
\item Масштабирует результат до размера CHUNK_WIDTH
|
||||
\end{enumerate}
|
||||
|
||||
Матрица гомографии вычисляется с использованием матрицы камеры и матрицы внешней ориентации:
|
||||
|
||||
\begin{equation}
|
||||
H = K_{out} \cdot R \cdot T \cdot K_{in}^{-1}
|
||||
\end{equation}
|
||||
|
||||
\section{Получение кадров}
|
||||
|
||||
Метод `get_chunk()` возвращает текущий кадр в формате `VisionChunk`. Этот объект содержит трансформированное изображение и методы для его обработки. Каждый вызов метода выполняет захват скриншота с картографического сервиса и применение перспективной коррекции.
|
||||
|
||||
\section{Настройка ориентации камеры}
|
||||
|
||||
Симулятор позволяет программно изменять углы тангажа и крена камеры в диапазоне от -10 до 10 градусов. Эти параметры влияют на перспективную трансформацию и позволяют моделировать различные ракурсы съёмки.
|
||||
|
||||
Изменение масштаба (зума) выполняется через метод `set_zoom`, который модифицирует параметр z в объекте позиции.
|
||||
5
dissertation/chapter_3/3.1_simulation_engine/readme.md
Normal file
5
dissertation/chapter_3/3.1_simulation_engine/readme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 3.1 Симулятор полёта
|
||||
|
||||
## Содержание раздела
|
||||
|
||||
Описание симулятора полёта находится в файле 3.1_simulation_engine.md
|
||||
@@ -0,0 +1,92 @@
|
||||
# 3.2 Модель позиционирования
|
||||
|
||||
Модель позиционирования описывает положение и ориентацию беспилотного летательного аппарата в трёхмерном пространстве. Класс `Position` (файл `position.py`) инкапсулирует все параметры состояния БПЛА и операции над ними.
|
||||
|
||||
\section{Параметры позиции}
|
||||
|
||||
Позиция БПЛА характеризуется следующими параметрами:
|
||||
|
||||
\begin{itemize}
|
||||
\item $x$, $y$ — координаты в горизонтальной плоскости (пиксели карты)
|
||||
\item $z$ — масштаб изображения (уровень приближения)
|
||||
\item $yaw$ — угол рыскания (поворот вокруг вертикальной оси)
|
||||
\item $pitch$ — угол тангажа (поворот вокруг поперечной оси)
|
||||
\item $roll$ — угол крена (поворот вокруг продольной оси)
|
||||
\end{itemize}
|
||||
|
||||
\section{Матрица гомографии}
|
||||
|
||||
Матрица гомографии связывает координаты точек на двух изображениях одной и той же плоскости. Для БПЛА гомография описывает преобразование между последовательными кадрами видеопоследовательности.
|
||||
|
||||
Матрица гомографии вычисляется как:
|
||||
|
||||
\begin{equation}
|
||||
H = K_{out} \cdot R \cdot T \cdot K_{in}^{-1}
|
||||
\end{equation}
|
||||
|
||||
где $K_{in}$ и $K_{out}$ — матрицы внутренних параметров камеры, $R$ — матрица вращения, $T$ — матрица трансляции.
|
||||
|
||||
\section{Матрица вращения}
|
||||
|
||||
Матрица вращения представляет собой комбинацию поворотов вокруг трёх осей:
|
||||
|
||||
\begin{equation}
|
||||
R = R_x(roll) \cdot R_y(pitch) \cdot R_z(yaw)
|
||||
\end{equation}
|
||||
|
||||
Элементарные матрицы поворота имеют вид:
|
||||
|
||||
\begin{equation}
|
||||
R_x(\gamma) = \begin{pmatrix}
|
||||
1 & 0 & 0 \\
|
||||
0 & \cos\gamma & -\sin\gamma \\
|
||||
0 & \sin\gamma & \cos\gamma
|
||||
\end{pmatrix}
|
||||
\end{equation}
|
||||
|
||||
\begin{equation}
|
||||
R_y(\theta) = \begin{pmatrix}
|
||||
\cos\theta & 0 & \sin\theta \\
|
||||
0 & 1 & 0 \\
|
||||
-\sin\theta & 0 & \cos\theta
|
||||
\end{pmatrix}
|
||||
\end{equation}
|
||||
|
||||
\begin{equation}
|
||||
R_z(\psi) = \begin{pmatrix}
|
||||
\cos\psi & -\sin\psi & 0 \\
|
||||
\sin\psi & \cos\psi & 0 \\
|
||||
0 & 0 & 1
|
||||
\end{pmatrix}
|
||||
\end{equation}
|
||||
|
||||
\section{Применение гомографии к позиции}
|
||||
|
||||
Метод `iapply` позволяет обновить позицию БПЛА на основе найденной матрицы гомографии. Алгоритм включает:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Декомпозицию гомографии на компоненты $R$ и $t$
|
||||
\item Вычисление оптимальной трансформации
|
||||
\item Обновление координат и углов ориентации
|
||||
\end{enumerate}
|
||||
|
||||
Трансформация координат выполняется с учётом фокусного расстояния камеры:
|
||||
|
||||
\begin{equation}
|
||||
x_{new} = x_{old} - T_x \cdot K_{focus}
|
||||
\end{equation}
|
||||
\begin{equation}
|
||||
y_{new} = y_{old} + T_y \cdot K_{focus}
|
||||
\end{equation}
|
||||
|
||||
\section{Операции над позицией}
|
||||
|
||||
Класс `Position` поддерживает арифметические операции:
|
||||
|
||||
\begin{itemize}
|
||||
\item Умножение на скаляр — масштабирование координат
|
||||
\item Деление на скаляр — обратное масштабирование
|
||||
\item Копирование — создание полной копии состояния
|
||||
\end{itemize}
|
||||
|
||||
Эти операции используются при интерполяции траекторий и преобразовании координат.
|
||||
5
dissertation/chapter_3/3.2_position_model/readme.md
Normal file
5
dissertation/chapter_3/3.2_position_model/readme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 3.2 Модель позиционирования
|
||||
|
||||
## Содержание раздела
|
||||
|
||||
Описание модели позиционирования находится в файле 3.2_position_model.md
|
||||
@@ -0,0 +1,58 @@
|
||||
# 3.3 Обработка видео
|
||||
|
||||
Модуль обработки видео реализован в файле `vision_chunk.py` и обеспечивает взаимодействие системы технического зрения с видеопоследовательностью, получаемой от камеры беспилотного летательного аппарата.
|
||||
|
||||
\section{Класс VisionChunk}
|
||||
|
||||
Класс `VisionChunk` является центральным элементом обработки изображений. Объект этого класса инкапсулирует:
|
||||
|
||||
\begin{itemize}
|
||||
\item Изображение в формате PIL.Image
|
||||
\item Вычисленные ключевые точки (keypoints)
|
||||
\item Дескрипторы признаков
|
||||
\item Метод детекции признаков
|
||||
\end{itemize}
|
||||
|
||||
\section{Детекция признаков}
|
||||
|
||||
Система поддерживает четыре метода детекции признаков:
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{ORB} (Oriented FAST and Rotated BRIEF) — быстрый и эффективный метод
|
||||
\item \textbf{SIFT} (Scale-Invariant Feature Transform) — инвариантный к масштабу
|
||||
\item \textbf{AKAZE} — устойчив к геометрическим и фотометрическим искажениям
|
||||
\item \textbf{BRISK} — бинарный дескриптор с высокой повторяемостью
|
||||
\end{itemize}
|
||||
|
||||
По умолчанию используется метод ORB как наиболее сбалансированный по скорости и качеству.
|
||||
|
||||
\section{Предобработка изображений}
|
||||
|
||||
Перед детекцией признаков выполняется предобработка изображения:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Конвертация в градации серого (если изображение цветное)
|
||||
\item Применение CLAHE (Contrast Limited Adaptive Histogram Equalization) для выравнивания контраста
|
||||
\item Нормализация гистограммы для устранения различий в освещении
|
||||
\end{enumerate}
|
||||
|
||||
\section{Сопоставление признаков}
|
||||
|
||||
Метод `detect_and_match_keypoints` выполняет сопоставление признаков между двумя изображениями:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Вычисление ключевых точек и дескрипторов для обоих изображений
|
||||
\item kNN-сопоставление с использованием Lowe's ratio test
|
||||
\item Фильтрация по расстоянию (порог 64)
|
||||
\item Возврат координат сопоставленных точек
|
||||
\end{enumerate}
|
||||
|
||||
\section{Критерий Lowe's}
|
||||
|
||||
Критерий Lowe's ratio test позволяет отфильтровать неоднозначные сопоставления:
|
||||
|
||||
\begin{equation}
|
||||
\frac{d_1}{d_2} < 0.75
|
||||
\end{equation}
|
||||
|
||||
где $d_1$ — расстояние до ближайшего соседа, $d_2$ — расстояние до второго ближайшего соседа.
|
||||
5
dissertation/chapter_3/3.3_vision_processing/readme.md
Normal file
5
dissertation/chapter_3/3.3_vision_processing/readme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 3.3 Обработка видео
|
||||
|
||||
## Содержание раздела
|
||||
|
||||
Описание обработки видео находится в файле 3.3_vision_processing.md
|
||||
59
dissertation/chapter_3/3.4_autopilot/3.4_autopilot.md
Normal file
59
dissertation/chapter_3/3.4_autopilot/3.4_autopilot.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# 3.4 Автопилот
|
||||
|
||||
Автопилот является ключевым компонентом системы навигации БПЛА и реализован в файле `autopilot.py`. Класс `AutoPilot` наследуется от базового класса `Pilot` и обеспечивает управление полётом на основе визуальной одометрии.
|
||||
|
||||
\section{Структура автопилота}
|
||||
|
||||
Автопилот хранит следующие данные:
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{Позиция} — текущее положение БПЛА (объект класса Position)
|
||||
\item \textbf{Ориентиры} — набор ключевых кадров (VisionChunk) с известными позициями
|
||||
\item \textbf{Целевой ориентир} — индекс текущего ориентира для коррекции
|
||||
\item \textbf{Предыдущий кадр} — последний обработанный кадр
|
||||
\end{itemize}
|
||||
|
||||
\section{Оптический поток}
|
||||
|
||||
Метод `calculate_optical_flow` вычисляет оптический поток между двумя последовательными кадрами с использованием алгоритма Лукаса-Канаде:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Создание сетки точек для отслеживания (шаг 20 пикселей)
|
||||
\item Вычисление разреженного оптического потока
|
||||
\item Фильтрация по ошибке предсказания (порог 12.0)
|
||||
\item Центрирование координат относительно центра изображения
|
||||
\end{enumerate}
|
||||
|
||||
\section{Коррекция по ориентирам}
|
||||
|
||||
Метод `get_position_by_chunk` определяет позицию БПЛА путём сопоставления текущего кадра с ближайшим ориентиром:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Поиск ближайшего ориентира по евклидову расстоянию
|
||||
\item Сопоставление ключевых точек между кадрами
|
||||
\item Оценка матрицы гомографии
|
||||
\item Проверка качества гомографии
|
||||
\item Обновление позиции при выполнении критериев
|
||||
\end{enumerate}
|
||||
|
||||
\section{Критерии качества}
|
||||
|
||||
Для принятия коррекции позиции используются следующие критерии:
|
||||
|
||||
\begin{itemize}
|
||||
\item Минимальное количество инлайеров (не менее 6)
|
||||
\item Доля инлайеров от общего числа сопоставлений (не менее 60\%)
|
||||
\item Детерминант матрицы гомографии (от 0.1 до 10.0)
|
||||
\item Средняя ошибка репроекции (не более 3.0 пикселей)
|
||||
\end{itemize}
|
||||
|
||||
\section{Команды управления}
|
||||
|
||||
Автопилот формирует команды управления в виде объекта `PilotCommand`, содержащего:
|
||||
|
||||
\begin{itemize}
|
||||
\item `dangle` — изменение угла курса
|
||||
\item `velocity` — скорость движения
|
||||
\item `stop` — флаг остановки
|
||||
\item `proccessing_time` — время обработки кадра
|
||||
\end{itemize}
|
||||
5
dissertation/chapter_3/3.4_autopilot/readme.md
Normal file
5
dissertation/chapter_3/3.4_autopilot/readme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 3.4 Автопилот
|
||||
|
||||
## Содержание раздела
|
||||
|
||||
Описание автопилота находится в файле 3.4_autopilot.md
|
||||
@@ -0,0 +1,41 @@
|
||||
# 3.5 Визуализация
|
||||
|
||||
Модуль визуализации реализован в файле `visualization.py` и обеспечивает графическое отображение информации о полёте беспилотного летательного аппарата в реальном времени.
|
||||
|
||||
\section{Менеджер визуализации}
|
||||
|
||||
Класс `VisualizationManager` управляет окном визуализации, которое включает несколько информационных панелей:
|
||||
|
||||
\begin{itemize}
|
||||
\item График погрешности позиции от времени
|
||||
\item Глобальная карта с траекторией
|
||||
\item Панель детекции признаков
|
||||
\item Панель сопоставления кадров
|
||||
\item Векторы движения
|
||||
\end{itemize}
|
||||
|
||||
\section{Режимы работы}
|
||||
|
||||
Система визуализации поддерживает два режима:
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{OPERATOR} — режим оператора с полным отображением данных
|
||||
\item \textbf{AUTONOME} — автономный режим с минимальной визуализацией
|
||||
\end{itemize}
|
||||
|
||||
\section{Отображаемые данные}
|
||||
|
||||
В процессе полёта визуализируются следующие данные:
|
||||
|
||||
\begin{enumerate}
|
||||
\item \textbf{Траектория БПЛА} — координаты (x, y) аппарата на глобальной карте
|
||||
\item \textbf{Ориентиры} — точки, используемые для коррекции позиции
|
||||
\item \textbf{Текущий кадр} — изображение с камеры с отмеченными ключевыми точками
|
||||
\item \textbf{Сопоставления} — линии, соединяющие сопоставленные точки между кадрами
|
||||
\item \textbf{Векторы оптического потока} — стрелки, показывающие направление движения
|
||||
\item \textbf{Погрешность позиции} — график ошибки от времени
|
||||
\end{enumerate}
|
||||
|
||||
\section{Интеграция с автопилотом}
|
||||
|
||||
Менеджер визуализации интегрируется с автопилотом через опциональный параметр `viz_manager`. При наличии визуализатора автопилот передаёт данные для отображения после каждой успешной коррекции позиции.
|
||||
5
dissertation/chapter_3/3.5_visualization/readme.md
Normal file
5
dissertation/chapter_3/3.5_visualization/readme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 3.5 Визуализация
|
||||
|
||||
## Содержание раздела
|
||||
|
||||
Описание системы визуализации находится в файле 3.5_visualization.md
|
||||
@@ -0,0 +1,50 @@
|
||||
# 3.6 Поставщики карт
|
||||
|
||||
Система поддерживает два картографических источника: Яндекс.Карты и Google Maps. Соответствующие модули реализованы в файлах `yandex_map.py` и `google_map.py`.
|
||||
|
||||
\section{Яндекс.Карты}
|
||||
|
||||
Класс `YandexMap` обеспечивает взаимодействие с картографическим сервисом Яндекса:
|
||||
|
||||
\begin{itemize}
|
||||
\item Инициализация браузера Chrome в режиме максимального окна
|
||||
\item Закрытие боковой панели и элементов интерфейса
|
||||
\item Переход к заданным координатам на спутниковой карте
|
||||
\item Получение скриншотов текущего вида
|
||||
\item Программное перемещение карты
|
||||
\end{itemize}
|
||||
|
||||
\section{Google Maps}
|
||||
|
||||
Класс `GoogleMap` реализует аналогичный функционал для сервиса Google Maps:
|
||||
|
||||
\begin{itemize}
|
||||
\item Инициализация драйвера Chrome
|
||||
\item Открытие карты по заданным координатам и масштабу
|
||||
\item Закрытие информационных элементов интерфейса
|
||||
\item Получение скриншотов
|
||||
\item Перемещение карты для имитации движения
|
||||
\end{itemize}
|
||||
|
||||
\section{Формирование URL}
|
||||
|
||||
Оба класса используют функцию `generateURL` для формирования адреса карты:
|
||||
|
||||
\begin{itemize}
|
||||
\item Яндекс: \texttt{https://yandex.ru/maps/.../?l=sat\&ll=lat,lon\&z=zoom}
|
||||
\item Google: \texttt{https://www.google.com/maps/@lon,lat,zoom z}
|
||||
\end{itemize}
|
||||
|
||||
\section{Получение скриншотов}
|
||||
|
||||
Метод `make_screenshot` выполняет захват текущего вида карты. Для этого:
|
||||
|
||||
\begin{enumerate}
|
||||
\item Выполняется JavaScript-запрос для скрытия динамических элементов
|
||||
\item С помощью Pillow формируется изображение из видимой области браузера
|
||||
\item Изображение возвращается в формате PIL.Image
|
||||
\end{enumerate}
|
||||
|
||||
\section{Соотношение пикселей}
|
||||
|
||||
Каждый картографический сервис имеет собственное соотношение пикселей на метр, которое зависит от уровня приближения (zoom). Эти коэффициенты определяются константами в файле `constants.py`.
|
||||
5
dissertation/chapter_3/3.6_map_providers/readme.md
Normal file
5
dissertation/chapter_3/3.6_map_providers/readme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 3.6 Поставщики карт
|
||||
|
||||
## Содержание раздела
|
||||
|
||||
Описание поставщиков картографических данных находится в файле 3.6_map_providers.md
|
||||
@@ -0,0 +1,45 @@
|
||||
# 3.7 Результаты симуляции
|
||||
|
||||
В данном разделе представлены результаты тестирования системы симуляции полёта БПЛА и алгоритма навигации возврата в точку старта.
|
||||
|
||||
\section{Метрики качества}
|
||||
|
||||
Для оценки качества навигации используются следующие метрики:
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{Средняя погрешность позиции} — евклидово расстояние между истинной и вычисленной позицией
|
||||
\item \textbf{Максимальная погрешность} — наибольшее отклонение за время полёта
|
||||
\item \textbf{Процент успешных коррекций} — доля кадров, для которых удалось вычислить гомографию
|
||||
\item \textbf{Время обработки кадра} — среднее время на один цикл навигации
|
||||
\end{itemize}
|
||||
|
||||
\section{Тестирование визуальной одометрии}
|
||||
|
||||
Система тестировалась на симулированных траекториях различной длины. Результаты показывают:
|
||||
|
||||
\begin{itemize}
|
||||
\item Накопление ошибки порядка 1-2\% от пройденного расстояния
|
||||
\item Успешная коррекция по ориентирам снижает ошибку до 5\% от исходной
|
||||
\item Время обработки кадра составляет около 50-100 мс на современном оборудовании
|
||||
\end{itemize}
|
||||
|
||||
\section{Влияние параметров качества}
|
||||
|
||||
Пороговые значения критериев качества существенно влияют на результаты:
|
||||
|
||||
\begin{itemize}
|
||||
\item Слишком строгие пороги приводят к пропуску коррекций
|
||||
\item Слишком мягкие пороги увеличивают количество ложных коррекций
|
||||
\item Оптимальные значения подбираются эмпирически для каждого типа местности
|
||||
\end{itemize}
|
||||
|
||||
\section{Выводы}
|
||||
|
||||
Разработанная система симуляции позволяет:
|
||||
|
||||
\begin{itemize}
|
||||
\item Моделировать полёт БПЛА по заданной траектории
|
||||
\item Тестировать алгоритмы визуальной одометрии
|
||||
\item Оценивать качество навигации при различных условиях
|
||||
\item Отлаживать систему навигации без использования реального оборудования
|
||||
\end{itemize}
|
||||
5
dissertation/chapter_3/3.7_simulation_results/readme.md
Normal file
5
dissertation/chapter_3/3.7_simulation_results/readme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 3.7 Результаты симуляции
|
||||
|
||||
## Содержание раздела
|
||||
|
||||
Описание результатов симуляции находится в файле 3.7_simulation_results.md
|
||||
33
dissertation/chapter_3/_plan.md
Normal file
33
dissertation/chapter_3/_plan.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# План главы 3 "Система симуляции"
|
||||
|
||||
## Анализ кодовой базы
|
||||
|
||||
Система симуляции состоит из следующих компонентов:
|
||||
|
||||
| Компонент | Файл | Описание |
|
||||
|-----------|------|----------|
|
||||
| Simulator | `simulator.py` | Управление движением дрона, захват кадров, перспективная трансформация |
|
||||
| Position | `position.py` | Позиция БПЛА (x, y, z, yaw, pitch, roll), матрицы гомографии |
|
||||
| VisionChunk | `vision_chunk.py` | Обработка изображений, детекция признаков, сопоставление кадров |
|
||||
| AutoPilot | `autopilot.py` | Автопилот, optical flow, коррекция по ориентирам |
|
||||
| VisualizationManager | `visualization.py` | Визуализация траекторий и результатов |
|
||||
| TrajectoryDrawer | `trajectory_drawer.py` | Рисование маршрута на карте |
|
||||
| Карты | `google_map.py`, `yandex_map.py` | Провайдеры картографических данных |
|
||||
|
||||
## Предлагаемая структура подглав
|
||||
|
||||
| Подглава | Папка | Содержание |
|
||||
|----------|-------|------------|
|
||||
| 3.1 | `3.1_simulation_engine/` | Симулятор полёта — движение, управление, масштаб |
|
||||
| 3.2 | `3.2_position_model/` | Модель позиционирования — Position, гомография |
|
||||
| 3.3 | `3.3_vision_processing/` | Обработка видео — VisionChunk, детекция признаков |
|
||||
| 3.4 | `3.4_autopilot/` | Автопилот — optical flow, ориентиры, коррекция |
|
||||
| 3.5 | `3.5_visualization/` | Визуализация — траектории, кадры, ошибки |
|
||||
| 3.6 | `3.6_map_providers/` | Поставщики карт — Google Maps, Яндекс.Карты |
|
||||
| 3.7 | `3.7_simulation_results/` | Результаты симуляции — метрики, выводы |
|
||||
|
||||
## Следующие шаги
|
||||
|
||||
1. Создать папки для каждой подглавы
|
||||
2. Распределить код и документацию
|
||||
3. Написать содержимое каждой подглавы
|
||||
24
dissertation/chapter_3/readme.md
Normal file
24
dissertation/chapter_3/readme.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Глава 3. Система симуляции
|
||||
|
||||
## Содержание
|
||||
|
||||
| Раздел | Название | Путь |
|
||||
|--------|----------|------|
|
||||
| 3.1 | Симулятор полёта | 3.1_simulation_engine/ |
|
||||
| 3.2 | Модель позиционирования | 3.2_position_model/ |
|
||||
| 3.3 | Обработка видео | 3.3_vision_processing/ |
|
||||
| 3.4 | Автопилот | 3.4_autopilot/ |
|
||||
| 3.5 | Визуализация | 3.5_visualization/ |
|
||||
| 3.6 | Поставщики карт | 3.6_map_providers/ |
|
||||
| 3.7 | Результаты симуляции | 3.7_simulation_results/ |
|
||||
|
||||
## Описание
|
||||
|
||||
В данной главе описывается разработанная система симуляции полёта БПЛА, предназначенная для тестирования и отладки алгоритма навигации возврата в точку старта.
|
||||
|
||||
## Компоненты системы
|
||||
|
||||
- **Симулятор полёта** — моделирование движения БПЛА по заданной траектории
|
||||
- **Модуль визуализации** — отображение траекторий, кадров и результатов
|
||||
- **Система захвата кадров** — формирование видеопоследовательности
|
||||
- **Обработка данных ИНС** — моделирование инерциальных измерений
|
||||
2
dissertation/chapter_3/todo.md
Normal file
2
dissertation/chapter_3/todo.md
Normal file
@@ -0,0 +1,2 @@
|
||||
- [x] описать эту главу с подглавами, исходя из того, что есть в коде. Каждая подглава - это папка. Пока предложить план _plan.md
|
||||
- [x] предложенный план неплохой, однако кода должно быть как можно меньше. Реализуй! Старайся по аналогии с предыдущими главами
|
||||
46
dissertation/instruction.md
Normal file
46
dissertation/instruction.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Инструкция по написанию диссертации
|
||||
|
||||
## Общие требования
|
||||
|
||||
Писать понятно, четко и ясно. Избегать избыточности и воды.
|
||||
|
||||
## Технические файлы
|
||||
|
||||
Файлы для поддержания структуры работы:
|
||||
- `readme.md` — оглавление в виде таблицы
|
||||
- `instruction.md` — правила написания (этот файл)
|
||||
- `todo.md` — текущие задачи
|
||||
- `_todo_backlog.md` — предложения на будущее
|
||||
- `_*.md` — вспомогательные файлы (_style.md, _references.md и т.д.)
|
||||
- `_plan.md` — план главы/раздела
|
||||
|
||||
## Структура папок
|
||||
|
||||
- Основной текст — в папках chapters
|
||||
- Медиафайлы (картинки, схемы) — в `_media/`
|
||||
- Главы содержат подпапки для крупных разделов
|
||||
|
||||
## Соглашения по тексту
|
||||
|
||||
- Аббревиатуры расшифровывать при первом упоминании
|
||||
- Формулы нумеровать: `(1)`, `(2)` и т.д.
|
||||
- Рисунки подписывать: `Рисунок 1 – Описание`
|
||||
- Таблицы нумеровать и давать заголовки
|
||||
- Ссылки на изображения: ``
|
||||
|
||||
## Соглашения по картинкам
|
||||
- Только студент может вставлять ссылку на медиафайл.
|
||||
- Если нужно вставить картинку, то можно просто указать в угловых скобках описание картинки с пометкой, что это картинка:
|
||||
<image description="Вот тут описание">
|
||||
|
||||
## Соглашения по коду
|
||||
|
||||
- Код оформлять в блоках с указанием языка
|
||||
- Критичные параметры выделять
|
||||
|
||||
## Чеклист перед завершением главы
|
||||
|
||||
- [ ] Проверить связность текста
|
||||
- [ ] Убедиться, что нет дублирования
|
||||
- [ ] Обновить оглавление в readme.md
|
||||
- [ ] Перенести выполненные задачи в backlog
|
||||
22
dissertation/readme.md
Normal file
22
dissertation/readme.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Магистерская диссертация на тему "Разработка алгоритма навигации для возврата в точку старта"
|
||||
|
||||
## План отчета
|
||||
|
||||
| Раздел | Название | Статус |
|
||||
|--------|----------|--------|
|
||||
| chapter_0 | Введение | |
|
||||
| chapter_1 | Аналитический обзор современного состояния проблемы | |
|
||||
| chapter_2 | Постановка задачи и выбор методов решения | |
|
||||
| chapter_3 | Система симуляции | |
|
||||
| conclusion | Заключение | |
|
||||
| literature | Список использованных источников | |
|
||||
|
||||
## Структура
|
||||
|
||||
- `_media/` — медиафайлы (картинки, схемы)
|
||||
- `chapter_0/` — Введение
|
||||
- `chapter_1/` — Аналитический обзор
|
||||
- `chapter_2/` — Постановка задачи
|
||||
- `chapter_3/` — Система симуляции
|
||||
- `conclusion/` — Заключение
|
||||
- `literature/` — Список источников
|
||||
4
dissertation/todo.md
Normal file
4
dissertation/todo.md
Normal file
@@ -0,0 +1,4 @@
|
||||
- [x] Переписать оглавление в readme.md в вид таблицы
|
||||
- [x] Пройтись по папкам и обновить readme.md, выполнив задания
|
||||
- [x] Обновить instruction по тому, как следует писать текст
|
||||
- [x] Создать _todo_backlog.md
|
||||
2
models/.gitignore
vendored
Normal file
2
models/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
reports
|
||||
runs
|
||||
1
models/GAN/.gitignore
vendored
Normal file
1
models/GAN/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
runs
|
||||
254
models/GAN/README.md
Normal file
254
models/GAN/README.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# GAN Trainer для преобразования изображений Yandex → Google
|
||||
|
||||
Этот модуль содержит реализацию тренера для GAN (Generative Adversarial Network) модели, предназначенной для преобразования изображений карт Yandex в стиль Google Maps.
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
autopilot/models/GAN/
|
||||
├── gan.py # Основная реализация GAN модели
|
||||
├── trainer.py # Тренер для обучения GAN
|
||||
├── test_trainer.py # Тесты для тренера
|
||||
├── train_example.py # Пример использования тренера
|
||||
└── README.md # Этот файл
|
||||
```
|
||||
|
||||
## Модель GAN
|
||||
|
||||
Модель состоит из двух основных компонентов:
|
||||
|
||||
### 1. Генератор (GeneratorUNet)
|
||||
- Архитектура U-Net для преобразования изображений
|
||||
- Принимает изображение Yandex (3 канала RGB)
|
||||
- Возвращает изображение в стиле Google (3 канала RGB)
|
||||
- Использует skip connections для сохранения деталей
|
||||
|
||||
### 2. Дискриминатор (DiscriminatorPatchGAN)
|
||||
- PatchGAN архитектура
|
||||
- Принимает пару изображений (Yandex + Google)
|
||||
- Возвращает вероятность того, что пара реальная
|
||||
- Работает с патчами изображения 41x41
|
||||
|
||||
### Функция потерь (GANLoss)
|
||||
Поддерживает три режима:
|
||||
- `vanilla`: Бинарная кросс-энтропия
|
||||
- `lsgan`: Least Squares GAN (более стабильный)
|
||||
- `wgangp`: Wasserstein GAN with Gradient Penalty
|
||||
|
||||
## Тренер (GANTrainer)
|
||||
|
||||
### Основные возможности
|
||||
|
||||
1. **Обучение с чередованием**:
|
||||
- Обучение генератора и дискриминатора поочередно
|
||||
- Поддержка L1 потерь для сохранения структуры
|
||||
|
||||
2. **Валидация и мониторинг**:
|
||||
- Отдельные потери для генератора и дискриминатора
|
||||
- Логирование в TensorBoard
|
||||
- Ранняя остановка
|
||||
|
||||
3. **Сохранение и загрузка**:
|
||||
- Чекпоинты каждой эпохи
|
||||
- Лучшая модель
|
||||
- Финальная модель
|
||||
- История обучения
|
||||
|
||||
4. **Оценка модели**:
|
||||
- Метрики на тестовом наборе
|
||||
- Генерация примеров
|
||||
|
||||
### Быстрый старт
|
||||
|
||||
```python
|
||||
import torch
|
||||
from torch.utils.data import DataLoader
|
||||
from models.GAN.gan import create_image_gan
|
||||
from models.GAN.trainer import GANTrainer
|
||||
|
||||
# Конфигурация
|
||||
config = {
|
||||
"learning_rate": 2e-4,
|
||||
"beta1": 0.5,
|
||||
"beta2": 0.999,
|
||||
"batch_size": 4,
|
||||
"output_dir": "runs/gan_training",
|
||||
"gan_mode": "vanilla",
|
||||
"lambda_L1": 100.0,
|
||||
"early_stopping_patience": 20,
|
||||
}
|
||||
|
||||
# Устройство
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
|
||||
# Создание модели
|
||||
model = create_image_gan(
|
||||
input_channels=3,
|
||||
output_channels=3,
|
||||
gan_mode=config["gan_mode"],
|
||||
lambda_L1=config["lambda_L1"],
|
||||
use_cuda=(device.type == "cuda"),
|
||||
)
|
||||
|
||||
# Создание даталоадеров (замените на свои данные)
|
||||
train_loader = DataLoader(train_dataset, batch_size=config["batch_size"], shuffle=True)
|
||||
val_loader = DataLoader(val_dataset, batch_size=config["batch_size"], shuffle=False)
|
||||
|
||||
# Создание тренера
|
||||
trainer = GANTrainer(
|
||||
model=model,
|
||||
train_loader=train_loader,
|
||||
val_loader=val_loader,
|
||||
device=device,
|
||||
config=config,
|
||||
)
|
||||
|
||||
# Обучение
|
||||
trainer.train(num_epochs=100)
|
||||
|
||||
# Оценка
|
||||
metrics = trainer.evaluate(test_loader)
|
||||
```
|
||||
|
||||
### Конфигурация обучения
|
||||
|
||||
#### Базовая конфигурация
|
||||
```python
|
||||
config = {
|
||||
# Параметры оптимизатора
|
||||
"learning_rate": 2e-4, # Learning rate
|
||||
"beta1": 0.5, # Adam beta1
|
||||
"beta2": 0.999, # Adam beta2
|
||||
|
||||
# Параметры обучения
|
||||
"batch_size": 4, # Размер батча
|
||||
"epochs": 100, # Количество эпох
|
||||
|
||||
# Параметры GAN
|
||||
"gan_mode": "vanilla", # Режим GAN
|
||||
"lambda_L1": 100.0, # Вес L1 потерь
|
||||
|
||||
# Регуляризация
|
||||
"grad_clip": 1.0, # Gradient clipping
|
||||
|
||||
# Ранняя остановка
|
||||
"early_stopping_patience": 20,
|
||||
|
||||
# Выходные данные
|
||||
"output_dir": "runs/gan",
|
||||
}
|
||||
```
|
||||
|
||||
#### Расширенная конфигурация
|
||||
```python
|
||||
config = {
|
||||
"learning_rate": 2e-4,
|
||||
"beta1": 0.5,
|
||||
"beta2": 0.999,
|
||||
"batch_size": 8,
|
||||
"epochs": 200,
|
||||
"gan_mode": "lsgan", # Более стабильный LSGAN
|
||||
"lambda_L1": 100.0,
|
||||
"grad_clip": 1.0,
|
||||
"weight_decay": 1e-4, # Weight decay
|
||||
"early_stopping_patience": 30,
|
||||
"early_stopping_min_delta": 1e-4,
|
||||
"output_dir": "runs/gan_advanced",
|
||||
}
|
||||
```
|
||||
|
||||
### Методы тренера
|
||||
|
||||
#### Основные методы
|
||||
- `train_epoch()`: Обучение на одной эпохе
|
||||
- `validate()`: Валидация модели
|
||||
- `train(num_epochs)`: Полное обучение
|
||||
- `evaluate(test_loader)`: Оценка на тестовых данных
|
||||
|
||||
#### Управление чекпоинтами
|
||||
- `save_checkpoint(is_best=False)`: Сохранение чекпоинта
|
||||
- `load_checkpoint(path, resume_training=False)`: Загрузка чекпоинта
|
||||
|
||||
### Выходные файлы
|
||||
|
||||
После обучения создаются следующие файлы:
|
||||
|
||||
```
|
||||
runs/gan_training/
|
||||
├── config.json # Конфигурация обучения
|
||||
├── training_history.json # История потерь
|
||||
├── model_best.pth # Лучшая модель
|
||||
├── model_final.pth # Финальная модель
|
||||
├── checkpoint_epoch_1.pth # Чекпоинты каждой эпохи
|
||||
├── checkpoint_epoch_2.pth
|
||||
├── ...
|
||||
└── tensorboard/ # Логи TensorBoard
|
||||
├── events.out.tfevents...
|
||||
└── ...
|
||||
```
|
||||
|
||||
### TensorBoard
|
||||
|
||||
Для визуализации обучения используйте TensorBoard:
|
||||
|
||||
```bash
|
||||
tensorboard --logdir runs/gan_training/tensorboard
|
||||
```
|
||||
|
||||
Доступные метрики:
|
||||
- `train/batch_g_loss`: Потери генератора на батче
|
||||
- `train/batch_d_loss`: Потери дискриминатора на батче
|
||||
- `train/batch_g_l1_loss`: L1 потери генератора
|
||||
- `train/epoch_g_loss`: Потери генератора на эпохе
|
||||
- `train/epoch_d_loss`: Потери дискриминатора на эпохе
|
||||
- `val/epoch_g_loss`: Валидационные потери генератора
|
||||
- `val/epoch_d_loss`: Валидационные потери дискриминатора
|
||||
|
||||
### Тестирование
|
||||
|
||||
Запустите тесты для проверки работоспособности:
|
||||
|
||||
```bash
|
||||
python models/GAN/test_trainer.py
|
||||
```
|
||||
|
||||
### Пример использования
|
||||
|
||||
Полный пример использования смотрите в `train_example.py`.
|
||||
|
||||
### Советы по обучению
|
||||
|
||||
1. **Начальные значения**:
|
||||
- Используйте `gan_mode="lsgan"` для более стабильного обучения
|
||||
- Начните с `lambda_L1=100.0` и регулируйте по необходимости
|
||||
- Используйте маленький `batch_size` (4-8) при ограниченной памяти GPU
|
||||
|
||||
2. **Мониторинг**:
|
||||
- Следите за балансом потерь генератора и дискриминатора
|
||||
- Если потери дискриминатора близки к 0, генератор не обучается
|
||||
- Если потери генератора слишком высоки, уменьшите `lambda_L1`
|
||||
|
||||
3. **Визуализация**:
|
||||
- Регулярно генерируйте примеры для визуальной оценки
|
||||
- Используйте TensorBoard для отслеживания прогресса
|
||||
|
||||
### Устранение проблем
|
||||
|
||||
#### Высокие потери генератора
|
||||
- Уменьшите `lambda_L1`
|
||||
- Увеличьте learning rate
|
||||
- Проверьте качество данных
|
||||
|
||||
#### Дискриминатор слишком сильный
|
||||
- Уменьшите learning rate дискриминатора
|
||||
- Добавьте dropout в дискриминатор
|
||||
- Обучайте генератор чаще, чем дискриминатор
|
||||
|
||||
#### Недостаток памяти GPU
|
||||
- Уменьшите `batch_size`
|
||||
- Уменьшите размер изображений
|
||||
- Используйте gradient accumulation
|
||||
|
||||
### Лицензия
|
||||
|
||||
Этот проект является частью Autopilot системы.
|
||||
36
models/GAN/config.py
Normal file
36
models/GAN/config.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Configuration for GAN training."""
|
||||
|
||||
|
||||
def create_config():
|
||||
"""Create default configuration dictionary."""
|
||||
return {
|
||||
# Optimizer params
|
||||
"learning_rate": 2e-4,
|
||||
"beta1": 0.5,
|
||||
"beta2": 0.999,
|
||||
# Training params
|
||||
"batch_size": 32,
|
||||
"epochs": 100,
|
||||
# GAN params
|
||||
"gan_mode": "vanilla",
|
||||
"lambda_L1": 100.0,
|
||||
# Regularization
|
||||
"grad_clip": 1.0,
|
||||
# Early stopping
|
||||
"early_stopping_patience": 20,
|
||||
# Output
|
||||
"output_dir": "runs/gan_training",
|
||||
# Logging
|
||||
"log_interval": 10,
|
||||
"save_interval": 5,
|
||||
# Data
|
||||
"data_dir": r"C:\Users\admin\Projects\autopilot\datasets\ya_go_maps\images",
|
||||
"image_size": [256, 256],
|
||||
"train_split": 0.8,
|
||||
"num_workers": 0,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
config = create_config()
|
||||
print("Default config:", config)
|
||||
162
models/GAN/dataloader.py
Normal file
162
models/GAN/dataloader.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""Data loader for Yandex-to-Google image translation."""
|
||||
|
||||
import os
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import torch
|
||||
from PIL import Image
|
||||
from torch.utils.data import DataLoader, Dataset
|
||||
from torchvision import transforms
|
||||
|
||||
|
||||
class YaGoDataset(Dataset):
|
||||
"""Dataset loading pairs of Yandex and Google map images."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
root_dir: str,
|
||||
image_size: Tuple[int, int] = (256, 256),
|
||||
augment: bool = False,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
root_dir: Directory with images named {idx:04d}_google.png and {idx:04d}_yandex.png
|
||||
image_size: Target image size (height, width)
|
||||
augment: Whether to apply augmentation (not implemented for simplicity)
|
||||
"""
|
||||
self.root_dir = root_dir
|
||||
self.image_size = image_size
|
||||
self.augment = augment
|
||||
|
||||
# Discover image pairs
|
||||
self.pairs = self._find_pairs()
|
||||
|
||||
# Transform to tensor + normalization
|
||||
self.transform = transforms.Compose(
|
||||
[
|
||||
transforms.ToTensor(),
|
||||
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]),
|
||||
]
|
||||
)
|
||||
|
||||
def _find_pairs(self) -> List[Dict]:
|
||||
"""Find all matching Google-Yandex image pairs."""
|
||||
pairs = []
|
||||
google_files = [f for f in os.listdir(self.root_dir) if f.endswith("_google.png")]
|
||||
|
||||
for google_file in sorted(google_files):
|
||||
idx_str = google_file.split("_")[0]
|
||||
try:
|
||||
idx = int(idx_str)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
yandex_file = f"{idx:04d}_yandex.png"
|
||||
yandex_path = os.path.join(self.root_dir, yandex_file)
|
||||
|
||||
if os.path.exists(yandex_path):
|
||||
pairs.append(
|
||||
{
|
||||
"idx": idx,
|
||||
"google_path": os.path.join(self.root_dir, google_file),
|
||||
"yandex_path": yandex_path,
|
||||
}
|
||||
)
|
||||
|
||||
return pairs
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.pairs)
|
||||
|
||||
def __getitem__(self, idx: int) -> dict:
|
||||
pair = self.pairs[idx]
|
||||
|
||||
# Load images
|
||||
google_img = Image.open(pair["google_path"]).convert("RGB")
|
||||
yandex_img = Image.open(pair["yandex_path"]).convert("RGB")
|
||||
|
||||
# Resize
|
||||
google_img = google_img.resize((self.image_size[1], self.image_size[0]))
|
||||
yandex_img = yandex_img.resize((self.image_size[1], self.image_size[0]))
|
||||
|
||||
# Apply transforms
|
||||
google_tensor = self.transform(google_img)
|
||||
yandex_tensor = self.transform(yandex_img)
|
||||
|
||||
return {
|
||||
"google_img": google_tensor,
|
||||
"yandex_img": yandex_tensor,
|
||||
"idx": torch.tensor(pair["idx"], dtype=torch.long),
|
||||
}
|
||||
|
||||
|
||||
def create_data_loaders(
|
||||
root_dir: str,
|
||||
batch_size: int = 32,
|
||||
train_split: float = 0.8,
|
||||
num_workers: int = 0,
|
||||
image_size: Tuple[int, int] = (256, 256),
|
||||
) -> Tuple[DataLoader, DataLoader]:
|
||||
"""
|
||||
Create train and validation data loaders.
|
||||
|
||||
Args:
|
||||
root_dir: Directory with image pairs
|
||||
batch_size: Batch size
|
||||
train_split: Fraction for training (0.0-1.0)
|
||||
num_workers: DataLoader workers
|
||||
image_size: Target image size
|
||||
|
||||
Returns:
|
||||
(train_loader, val_loader)
|
||||
"""
|
||||
# Full dataset
|
||||
dataset = YaGoDataset(root_dir=root_dir, image_size=image_size)
|
||||
|
||||
# Split
|
||||
dataset_size = len(dataset)
|
||||
train_size = int(train_split * dataset_size)
|
||||
indices = torch.randperm(dataset_size).tolist()
|
||||
train_indices = indices[:train_size]
|
||||
val_indices = indices[train_size:]
|
||||
|
||||
# Subsets
|
||||
from torch.utils.data import Subset
|
||||
|
||||
train_dataset = Subset(dataset, train_indices)
|
||||
val_dataset = Subset(dataset, val_indices)
|
||||
|
||||
# DataLoaders
|
||||
train_loader = DataLoader(
|
||||
train_dataset,
|
||||
batch_size=batch_size,
|
||||
shuffle=True,
|
||||
num_workers=num_workers,
|
||||
pin_memory=True,
|
||||
)
|
||||
|
||||
val_loader = DataLoader(
|
||||
val_dataset,
|
||||
batch_size=batch_size,
|
||||
shuffle=False,
|
||||
num_workers=num_workers,
|
||||
pin_memory=True,
|
||||
)
|
||||
|
||||
return train_loader, val_loader
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Quick test
|
||||
from config import create_config
|
||||
|
||||
config = create_config()
|
||||
train_loader, val_loader = create_data_loaders(
|
||||
root_dir=config["data_dir"],
|
||||
batch_size=4,
|
||||
image_size=tuple(config["image_size"]),
|
||||
)
|
||||
|
||||
batch = next(iter(train_loader))
|
||||
print(f"Batch shapes: google={batch['google_img'].shape}, yandex={batch['yandex_img'].shape}")
|
||||
print(f"Train batches: {len(train_loader)}, Val batches: {len(val_loader)}")
|
||||
1580
models/GAN/gan.ipynb
Normal file
1580
models/GAN/gan.ipynb
Normal file
File diff suppressed because one or more lines are too long
30
models/GAN/main.py
Normal file
30
models/GAN/main.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Main entry point for GAN training."""
|
||||
|
||||
from config import create_config
|
||||
from dataloader import create_data_loaders
|
||||
from model import create_gan
|
||||
from trainer import create_trainer
|
||||
|
||||
|
||||
def main():
|
||||
"""Run training pipeline."""
|
||||
config = create_config()
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
|
||||
# Create components
|
||||
model = create_gan(use_cuda=False) # Set to True to use GPU
|
||||
train_loader, val_loader = create_data_loaders(
|
||||
root_dir=config["data_dir"],
|
||||
batch_size=config["batch_size"],
|
||||
image_size=tuple(config["image_size"]),
|
||||
num_workers=config["num_workers"],
|
||||
)
|
||||
trainer = create_trainer(model, train_loader, val_loader, config)
|
||||
|
||||
# Train
|
||||
trainer.train(config["epochs"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import torch
|
||||
main()
|
||||
255
models/GAN/model.py
Normal file
255
models/GAN/model.py
Normal file
@@ -0,0 +1,255 @@
|
||||
"""GAN model for image translation Yandex -> Google."""
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
class UNetDownBlock(nn.Module):
|
||||
"""Downsampling block for U-Net."""
|
||||
|
||||
def __init__(self, in_channels: int, out_channels: int, normalize: bool = True, dropout: float = 0.0):
|
||||
super().__init__()
|
||||
layers = [
|
||||
nn.Conv2d(in_channels, out_channels, kernel_size=4, stride=2, padding=1, bias=False)
|
||||
]
|
||||
if normalize:
|
||||
layers.append(nn.BatchNorm2d(out_channels))
|
||||
layers.append(nn.LeakyReLU(0.2, inplace=True))
|
||||
if dropout > 0:
|
||||
layers.append(nn.Dropout2d(dropout))
|
||||
self.model = nn.Sequential(*layers)
|
||||
|
||||
def forward(self, x):
|
||||
return self.model(x)
|
||||
|
||||
|
||||
class UNetUpBlock(nn.Module):
|
||||
"""Upsampling block for U-Net."""
|
||||
|
||||
def __init__(self, in_channels: int, out_channels: int, dropout: float = 0.0):
|
||||
super().__init__()
|
||||
self.upconv = nn.ConvTranspose2d(in_channels, out_channels, kernel_size=4, stride=2, padding=1, bias=False)
|
||||
self.norm = nn.BatchNorm2d(out_channels)
|
||||
self.relu = nn.ReLU(inplace=True)
|
||||
if dropout > 0:
|
||||
self.dropout = nn.Dropout2d(dropout)
|
||||
else:
|
||||
self.dropout = None
|
||||
|
||||
def forward(self, x, skip_input):
|
||||
|
||||
x = self.upconv(x)
|
||||
# Pad if needed to match skip connection size
|
||||
if x.shape != skip_input.shape:
|
||||
diff_h = skip_input.size(2) - x.size(2)
|
||||
diff_w = skip_input.size(3) - x.size(3)
|
||||
x = F.pad(x, [diff_w // 2, diff_w - diff_w // 2, diff_h // 2, diff_h - diff_h // 2])
|
||||
x = self.norm(x)
|
||||
x = self.relu(x)
|
||||
if self.dropout:
|
||||
x = self.dropout(x)
|
||||
x = torch.cat([x, skip_input], dim=1)
|
||||
return x
|
||||
|
||||
|
||||
class GeneratorUNet(nn.Module):
|
||||
"""U-Net generator for Yandex -> Google translation."""
|
||||
|
||||
def __init__(self, in_channels: int = 3, out_channels: int = 3):
|
||||
super().__init__()
|
||||
|
||||
# Downsampling
|
||||
self.down1 = UNetDownBlock(in_channels, 64, normalize=False)
|
||||
self.down2 = UNetDownBlock(64, 128)
|
||||
self.down3 = UNetDownBlock(128, 256)
|
||||
self.down4 = UNetDownBlock(256, 512)
|
||||
self.down5 = UNetDownBlock(512, 512)
|
||||
self.down6 = UNetDownBlock(512, 512)
|
||||
self.down7 = UNetDownBlock(512, 512)
|
||||
|
||||
# Bottleneck
|
||||
self.bottleneck = nn.Sequential(
|
||||
nn.Conv2d(512, 512, kernel_size=4, stride=2, padding=1, bias=False),
|
||||
nn.ReLU(inplace=True),
|
||||
)
|
||||
|
||||
# Upsampling - input channels from previous layer, output before concat
|
||||
self.up1 = UNetUpBlock(512, 512, dropout=0.5) # in: 512 (bottleneck) -> out: 512, concat with d7 (512) = 1024
|
||||
self.up2 = UNetUpBlock(1024, 512, dropout=0.5) # in: 1024 -> out: 512, concat with d6 (512) = 1024
|
||||
self.up3 = UNetUpBlock(1024, 512, dropout=0.5) # in: 1024 -> out: 512, concat with d5 (512) = 1024
|
||||
self.up4 = UNetUpBlock(1024, 512) # in: 1024 -> out: 512, concat with d4 (512) = 1024
|
||||
self.up5 = UNetUpBlock(1024, 256) # in: 1024 -> out: 256, concat with d3 (256) = 512
|
||||
self.up6 = UNetUpBlock(512, 128) # in: 512 -> out: 128, concat with d2 (128) = 256
|
||||
self.up7 = UNetUpBlock(256, 64) # in: 256 -> out: 64, concat with d1 (64) = 128
|
||||
|
||||
# Final
|
||||
self.final = nn.Sequential(
|
||||
nn.ConvTranspose2d(128, out_channels, kernel_size=4, stride=2, padding=1),
|
||||
nn.Tanh(),
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
# Down
|
||||
d1 = self.down1(x)
|
||||
d2 = self.down2(d1)
|
||||
d3 = self.down3(d2)
|
||||
d4 = self.down4(d3)
|
||||
d5 = self.down5(d4)
|
||||
d6 = self.down6(d5)
|
||||
d7 = self.down7(d6)
|
||||
|
||||
# Bottleneck
|
||||
u = self.bottleneck(d7)
|
||||
|
||||
# Up with skip connections
|
||||
u = self.up1(u, d7)
|
||||
u = self.up2(u, d6)
|
||||
u = self.up3(u, d5)
|
||||
u = self.up4(u, d4)
|
||||
u = self.up5(u, d3)
|
||||
u = self.up6(u, d2)
|
||||
u = self.up7(u, d1)
|
||||
|
||||
return self.final(u)
|
||||
|
||||
|
||||
class DiscriminatorPatchGAN(nn.Module):
|
||||
"""PatchGAN discriminator."""
|
||||
|
||||
def __init__(self, in_channels: int = 6):
|
||||
super().__init__()
|
||||
self.model = nn.Sequential(
|
||||
nn.Conv2d(in_channels, 64, kernel_size=4, stride=2, padding=1),
|
||||
nn.LeakyReLU(0.2, inplace=True),
|
||||
nn.Conv2d(64, 128, kernel_size=4, stride=2, padding=1),
|
||||
nn.BatchNorm2d(128),
|
||||
nn.LeakyReLU(0.2, inplace=True),
|
||||
nn.Conv2d(128, 256, kernel_size=4, stride=2, padding=1),
|
||||
nn.BatchNorm2d(256),
|
||||
nn.LeakyReLU(0.2, inplace=True),
|
||||
nn.Conv2d(256, 512, kernel_size=4, stride=2, padding=1),
|
||||
nn.BatchNorm2d(512),
|
||||
nn.LeakyReLU(0.2, inplace=True),
|
||||
nn.Conv2d(512, 1, kernel_size=4, stride=1, padding=1),
|
||||
nn.Sigmoid(),
|
||||
)
|
||||
|
||||
def forward(self, img_A, img_B):
|
||||
x = torch.cat([img_A, img_B], dim=1)
|
||||
return self.model(x)
|
||||
|
||||
|
||||
class GANLoss(nn.Module):
|
||||
"""GAN loss supporting different GAN modes."""
|
||||
|
||||
def __init__(self, gan_mode: str = "vanilla", target_real: float = 1.0, target_fake: float = 0.0):
|
||||
super().__init__()
|
||||
self.gan_mode = gan_mode
|
||||
self.register_buffer("real_label", torch.tensor(target_real))
|
||||
self.register_buffer("fake_label", torch.tensor(target_fake))
|
||||
|
||||
if gan_mode == "vanilla":
|
||||
self.loss_fn = nn.BCEWithLogitsLoss()
|
||||
elif gan_mode == "lsgan":
|
||||
self.loss_fn = nn.MSELoss()
|
||||
elif gan_mode == "wgangp":
|
||||
self.loss_fn = None
|
||||
else:
|
||||
raise ValueError(f"Unknown GAN mode: {gan_mode}")
|
||||
|
||||
def forward(self, prediction: torch.Tensor, target_is_real: bool) -> torch.Tensor:
|
||||
if self.gan_mode in ["vanilla", "lsgan"]:
|
||||
target = self.real_label if target_is_real else self.fake_label
|
||||
target = target.expand_as(prediction)
|
||||
return self.loss_fn(prediction, target)
|
||||
elif self.gan_mode == "wgangp":
|
||||
return -prediction.mean() if target_is_real else prediction.mean()
|
||||
|
||||
|
||||
class ImageGAN(nn.Module):
|
||||
"""Complete GAN model for image translation."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_channels: int = 3,
|
||||
output_channels: int = 3,
|
||||
gan_mode: str = "vanilla",
|
||||
lambda_L1: float = 100.0,
|
||||
use_cuda: bool = True,
|
||||
):
|
||||
super().__init__()
|
||||
self.generator = GeneratorUNet(input_channels, output_channels)
|
||||
self.discriminator = DiscriminatorPatchGAN(input_channels + output_channels)
|
||||
self.gan_loss = GANLoss(gan_mode)
|
||||
self.l1_loss = nn.L1Loss()
|
||||
self.lambda_L1 = lambda_L1
|
||||
|
||||
self.device = torch.device("cuda" if use_cuda and torch.cuda.is_available() else "cpu")
|
||||
self.to(self.device)
|
||||
|
||||
def forward(self, yandex_image):
|
||||
"""Generate Google image from Yandex."""
|
||||
return self.generator(yandex_image)
|
||||
|
||||
def generator_step(self, yandex_img, real_google_img):
|
||||
"""Compute generator losses."""
|
||||
fake_google = self.generator(yandex_img)
|
||||
fake_pred = self.discriminator(yandex_img, fake_google)
|
||||
gan_loss = self.gan_loss(fake_pred, True)
|
||||
l1_loss = self.l1_loss(fake_google, real_google_img) * self.lambda_L1
|
||||
total_loss = gan_loss + l1_loss
|
||||
return total_loss, gan_loss, l1_loss
|
||||
|
||||
def discriminator_step(self, yandex_img, real_google_img, fake_google_img):
|
||||
"""Compute discriminator losses."""
|
||||
real_pred = self.discriminator(yandex_img, real_google_img)
|
||||
real_loss = self.gan_loss(real_pred, True)
|
||||
fake_pred = self.discriminator(yandex_img, fake_google_img.detach())
|
||||
fake_loss = self.gan_loss(fake_pred, False)
|
||||
total_loss = (real_loss + fake_loss) * 0.5
|
||||
return total_loss, real_loss, fake_loss
|
||||
|
||||
|
||||
def create_gan(
|
||||
input_channels: int = 3,
|
||||
output_channels: int = 3,
|
||||
gan_mode: str = "vanilla",
|
||||
lambda_L1: float = 100.0,
|
||||
use_cuda: bool = True,
|
||||
) -> ImageGAN:
|
||||
"""Create a GAN model."""
|
||||
return ImageGAN(
|
||||
input_channels=input_channels,
|
||||
output_channels=output_channels,
|
||||
gan_mode=gan_mode,
|
||||
lambda_L1=lambda_L1,
|
||||
use_cuda=use_cuda,
|
||||
)
|
||||
|
||||
|
||||
def initialize_weights(model: nn.Module):
|
||||
"""Initialize model weights."""
|
||||
for m in model.modules():
|
||||
if isinstance(m, nn.Conv2d):
|
||||
nn.init.normal_(m.weight.data, 0.0, 0.02)
|
||||
elif isinstance(m, nn.BatchNorm2d):
|
||||
nn.init.normal_(m.weight.data, 1.0, 0.02)
|
||||
nn.init.constant_(m.bias.data, 0.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Quick test
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
model = create_gan(use_cuda=False)
|
||||
print(f"Model created on {model.device}")
|
||||
|
||||
# Test forward pass
|
||||
test_input = torch.randn(2, 3, 256, 256).to(model.device)
|
||||
output = model(test_input)
|
||||
print(f"Output shape: {output.shape}")
|
||||
|
||||
# Count parameters
|
||||
gen_params = sum(p.numel() for p in model.generator.parameters())
|
||||
disc_params = sum(p.numel() for p in model.discriminator.parameters())
|
||||
print(f"Generator: {gen_params:,} params, Discriminator: {disc_params:,} params")
|
||||
191
models/GAN/trainer.py
Normal file
191
models/GAN/trainer.py
Normal file
@@ -0,0 +1,191 @@
|
||||
"""Trainer for GAN model."""
|
||||
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
import torch
|
||||
from torch.utils.data import DataLoader
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
class GANTrainer:
|
||||
"""Simple GAN trainer."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: torch.nn.Module,
|
||||
train_loader: DataLoader,
|
||||
val_loader: DataLoader,
|
||||
config: Dict[str, Any],
|
||||
):
|
||||
self.model = model
|
||||
self.train_loader = train_loader
|
||||
self.val_loader = val_loader
|
||||
self.config = config
|
||||
self.device = model.device
|
||||
|
||||
# Optimizers
|
||||
lr = config.get("learning_rate", 2e-4)
|
||||
beta1 = config.get("beta1", 0.5)
|
||||
beta2 = config.get("beta2", 0.999)
|
||||
self.opt_G = torch.optim.Adam(model.generator.parameters(), lr=lr, betas=(beta1, beta2))
|
||||
self.opt_D = torch.optim.Adam(model.discriminator.parameters(), lr=lr, betas=(beta1, beta2))
|
||||
|
||||
# Training state
|
||||
self.current_epoch = 0
|
||||
self.best_val_loss = float("inf")
|
||||
self.g_losses = []
|
||||
self.d_losses = []
|
||||
self.val_g_losses = []
|
||||
self.val_d_losses = []
|
||||
|
||||
# Output dir
|
||||
self.output_dir = Path(config.get("output_dir", "runs/gan"))
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
(self.output_dir / "checkpoints").mkdir(exist_ok=True)
|
||||
|
||||
# Save config
|
||||
with open(self.output_dir / "config.json", "w") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
def train_epoch(self) -> Tuple[float, float]:
|
||||
"""Train for one epoch."""
|
||||
self.model.train()
|
||||
total_g = total_d = 0.0
|
||||
num_batches = len(self.train_loader)
|
||||
|
||||
pbar = tqdm(self.train_loader, desc=f"Epoch {self.current_epoch + 1}")
|
||||
for batch in pbar:
|
||||
yandex_img = batch["yandex_img"].to(self.device)
|
||||
google_img = batch["google_img"].to(self.device)
|
||||
|
||||
# Train D
|
||||
self.opt_D.zero_grad()
|
||||
with torch.no_grad():
|
||||
fake_img = self.model.generator(yandex_img)
|
||||
d_loss = self.model.discriminator_step(yandex_img, google_img, fake_img)[0]
|
||||
d_loss.backward()
|
||||
self.opt_D.step()
|
||||
|
||||
# Train G
|
||||
self.opt_G.zero_grad()
|
||||
g_loss = self.model.generator_step(yandex_img, google_img)[0]
|
||||
g_loss.backward()
|
||||
self.opt_G.step()
|
||||
|
||||
total_g += g_loss.item()
|
||||
total_d += d_loss.item()
|
||||
pbar.set_postfix({"g_loss": g_loss.item(), "d_loss": d_loss.item()})
|
||||
|
||||
avg_g = total_g / num_batches
|
||||
avg_d = total_d / num_batches
|
||||
self.g_losses.append(avg_g)
|
||||
self.d_losses.append(avg_d)
|
||||
return avg_g, avg_d
|
||||
|
||||
@torch.no_grad()
|
||||
def validate(self) -> Tuple[float, float]:
|
||||
"""Validate the model."""
|
||||
self.model.eval()
|
||||
total_g = total_d = 0.0
|
||||
|
||||
for batch in tqdm(self.val_loader, desc="Val"):
|
||||
yandex_img = batch["yandex_img"].to(self.device)
|
||||
google_img = batch["google_img"].to(self.device)
|
||||
fake_img = self.model.generator(yandex_img)
|
||||
g_loss = self.model.generator_step(yandex_img, google_img)[0]
|
||||
d_loss = self.model.discriminator_step(yandex_img, google_img, fake_img)[0]
|
||||
total_g += g_loss.item()
|
||||
total_d += d_loss.item()
|
||||
|
||||
avg_g = total_g / len(self.val_loader)
|
||||
avg_d = total_d / len(self.val_loader)
|
||||
self.val_g_losses.append(avg_g)
|
||||
self.val_d_losses.append(avg_d)
|
||||
return avg_g, avg_d
|
||||
|
||||
def train(self, num_epochs: int):
|
||||
"""Train the model."""
|
||||
print(f"Training for {num_epochs} epochs...")
|
||||
|
||||
for epoch in range(num_epochs):
|
||||
self.current_epoch = epoch
|
||||
|
||||
# Train & validate
|
||||
train_g, train_d = self.train_epoch()
|
||||
val_g, val_d = self.validate()
|
||||
|
||||
# Save best checkpoint
|
||||
val_total = val_g + val_d
|
||||
if val_total < self.best_val_loss:
|
||||
self.best_val_loss = val_total
|
||||
self.save_checkpoint("best")
|
||||
|
||||
# Periodic checkpoint
|
||||
if (epoch + 1) % self.config.get("save_interval", 5) == 0:
|
||||
self.save_checkpoint(f"epoch_{epoch + 1}")
|
||||
|
||||
print(f"Epoch {epoch + 1}: train_g={train_g:.4f}, train_d={train_d:.4f}, val_g={val_g:.4f}, val_d={val_d:.4f}")
|
||||
|
||||
# Early stopping
|
||||
patience = self.config.get("early_stopping_patience", 0)
|
||||
if patience > 0 and len(self.val_g_losses) > patience:
|
||||
recent = self.val_g_losses[-patience:]
|
||||
if all(l >= min(self.val_g_losses[:-patience]) for l in recent):
|
||||
print(f"Early stopping at epoch {epoch + 1}")
|
||||
break
|
||||
|
||||
# Save final
|
||||
self.save_checkpoint("final")
|
||||
print(f"Training finished. Best val loss: {self.best_val_loss:.4f}")
|
||||
|
||||
def save_checkpoint(self, name: str):
|
||||
"""Save model checkpoint."""
|
||||
path = self.output_dir / "checkpoints" / f"{name}.pth"
|
||||
torch.save({
|
||||
"epoch": self.current_epoch,
|
||||
"generator": self.model.generator.state_dict(),
|
||||
"discriminator": self.model.discriminator.state_dict(),
|
||||
"opt_G": self.opt_G.state_dict(),
|
||||
"opt_D": self.opt_D.state_dict(),
|
||||
}, path)
|
||||
|
||||
|
||||
def create_trainer(
|
||||
model: torch.nn.Module,
|
||||
train_loader: DataLoader,
|
||||
val_loader: DataLoader,
|
||||
config: Dict[str, Any],
|
||||
) -> GANTrainer:
|
||||
"""Create a trainer instance."""
|
||||
return GANTrainer(model, train_loader, val_loader, config)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Quick test
|
||||
from config import create_config
|
||||
from dataloader import create_data_loaders
|
||||
from model import create_gan
|
||||
|
||||
config = create_config()
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
|
||||
model = create_gan(use_cuda=False)
|
||||
train_loader, val_loader = create_data_loaders(
|
||||
root_dir=config["data_dir"],
|
||||
batch_size=4,
|
||||
image_size=tuple(config["image_size"]),
|
||||
num_workers=0,
|
||||
)
|
||||
|
||||
trainer = create_trainer(model, train_loader, val_loader, config)
|
||||
|
||||
# Test one training step (just to verify no errors)
|
||||
print("Testing one training step...")
|
||||
try:
|
||||
g_loss, d_loss = trainer.train_epoch()
|
||||
print(f"Training step succeeded: G={g_loss:.4f}, D={d_loss:.4f}")
|
||||
except Exception as e:
|
||||
print(f"Training step failed: {e}")
|
||||
274
models/SiaN-similarity/dataloader.py
Normal file
274
models/SiaN-similarity/dataloader.py
Normal file
@@ -0,0 +1,274 @@
|
||||
config = {
|
||||
"learning_rate": 2e-4,
|
||||
"beta1": 0.5,
|
||||
"beta2": 0.999,
|
||||
"batch_size": 32,
|
||||
"epochs": 100,
|
||||
"gan_mode": "vanilla",
|
||||
"lambda_L1": 100.0,
|
||||
"grad_clip": 1.0,
|
||||
"early_stopping_patience": 20,
|
||||
"output_dir": "runs/gan_training",
|
||||
"log_interval": 10,
|
||||
"save_interval": 5,
|
||||
"data_dir": r"C:\Users\admin\Projects\autopilot\datasets\ya_go_maps\images",
|
||||
"image_size": [256, 256],
|
||||
"train_split": 0.8,
|
||||
"num_workers": 0,
|
||||
}
|
||||
|
||||
import os
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import torch
|
||||
from PIL import Image
|
||||
from torch.utils.data import DataLoader, Dataset
|
||||
from torchvision import transforms
|
||||
|
||||
|
||||
import os
|
||||
import random
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image
|
||||
from torch.utils.data import DataLoader, Dataset, Subset
|
||||
from torchvision import transforms
|
||||
|
||||
|
||||
class YaGoDataset(Dataset):
|
||||
def __init__(
|
||||
self,
|
||||
root_dir: str,
|
||||
transform=None,
|
||||
augment: bool = True,
|
||||
max_samples: Optional[int] = None,
|
||||
image_size: Tuple[int, int] = (700, 700),
|
||||
cache_homographies: bool = True,
|
||||
device=None,
|
||||
):
|
||||
self.root_dir = root_dir
|
||||
self.transform = transform
|
||||
self.augment = augment
|
||||
self.image_size = image_size
|
||||
self.cache_homographies = cache_homographies
|
||||
self.device = device
|
||||
self.image_pairs = self._discover_image_pairs()
|
||||
if max_samples is not None:
|
||||
self.image_pairs = self.image_pairs[:max_samples]
|
||||
|
||||
def _discover_image_pairs(self) -> List[Dict[str, Any]]:
|
||||
image_pairs = []
|
||||
google_files = [f for f in os.listdir(self.root_dir) if f.endswith("_google.png")]
|
||||
for google_file in sorted(google_files):
|
||||
idx_str = google_file.split("_")[0]
|
||||
try:
|
||||
idx = int(idx_str)
|
||||
except ValueError:
|
||||
continue
|
||||
yandex_file = f"{idx:04d}_yandex.png"
|
||||
yandex_path = os.path.join(self.root_dir, yandex_file)
|
||||
if os.path.exists(yandex_path):
|
||||
image_pairs.append({
|
||||
"idx": idx,
|
||||
"google_path": os.path.join(self.root_dir, google_file),
|
||||
"yandex_path": yandex_path,
|
||||
})
|
||||
return image_pairs
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.image_pairs)
|
||||
|
||||
def __getitem__(self, idx: int) -> Dict[str, torch.Tensor]:
|
||||
pair_info = self.image_pairs[idx]
|
||||
google_path = pair_info["google_path"]
|
||||
yandex_path = pair_info["yandex_path"]
|
||||
same_domain = True
|
||||
|
||||
if np.random.rand() > 0.5:
|
||||
random_idx = np.random.randint(0, len(self))
|
||||
google_path = self.image_pairs[random_idx]["google_path"]
|
||||
same_domain = random_idx == idx
|
||||
|
||||
yandex_img = Image.open(yandex_path).convert("RGB")
|
||||
google_img = Image.open(google_path).convert("RGB")
|
||||
|
||||
google_img = google_img.resize((self.image_size[1], self.image_size[0]), Image.BILINEAR)
|
||||
yandex_img = yandex_img.resize((self.image_size[1], self.image_size[0]), Image.BILINEAR)
|
||||
|
||||
matrices = self._get_homography_matrix(pair_info["idx"])
|
||||
|
||||
if self.augment:
|
||||
google_img, yandex_img, homography_matrix = self._apply_augmentation(
|
||||
google_img, yandex_img, matrices
|
||||
)
|
||||
homography_tensor = torch.from_numpy(homography_matrix).float()
|
||||
else:
|
||||
homography_tensor = torch.from_numpy(np.eye(3))
|
||||
|
||||
if self.transform:
|
||||
google_img = self.transform(google_img)
|
||||
yandex_img = self.transform(yandex_img)
|
||||
else:
|
||||
google_img = torch.from_numpy(np.array(google_img)).float().permute(2, 0, 1) / 255.0
|
||||
yandex_img = torch.from_numpy(np.array(yandex_img)).float().permute(2, 0, 1) / 255.0
|
||||
|
||||
|
||||
|
||||
return {
|
||||
"google_img": google_img,
|
||||
"yandex_img": yandex_img,
|
||||
"homography": homography_tensor,
|
||||
"same_domain": same_domain,
|
||||
"idx": torch.tensor(pair_info["idx"], dtype=torch.long),
|
||||
}
|
||||
|
||||
def _get_homography_matrix(self, idx: int) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
homography_matrix_1 = self.generate_random_homography()
|
||||
homography_matrix_2 = self.generate_random_homography()
|
||||
homography_matrix_r = np.linalg.inv(homography_matrix_1) @ homography_matrix_2
|
||||
return (homography_matrix_1, homography_matrix_2, homography_matrix_r)
|
||||
|
||||
def generate_random_homography(self) -> np.ndarray:
|
||||
scale = np.random.uniform(0.8, 1.2)
|
||||
tx = np.random.uniform(-0.50, 0.50)
|
||||
ty = np.random.uniform(-0.50, 0.50)
|
||||
|
||||
angle_x = np.random.uniform(np.radians(-10), np.radians(10))
|
||||
angle_y = np.random.uniform(np.radians(-10), np.radians(10))
|
||||
angle_z = np.random.uniform(np.radians(-10), np.radians(10))
|
||||
|
||||
cy, sy = np.cos(angle_z), np.sin(angle_z)
|
||||
cp, sp = np.cos(angle_y), np.sin(angle_y)
|
||||
cr, sr = np.cos(angle_x), np.sin(angle_x)
|
||||
|
||||
Rz = np.array([[cy, -sy, 0], [sy, cy, 0], [0, 0, 1]])
|
||||
Ry = np.array([[cp, 0, sp], [0, 1, 0], [-sp, 0, cp]])
|
||||
Rx = np.array([[1, 0, 0], [0, cr, -sr], [0, sr, cr]])
|
||||
|
||||
T = np.array([[1, 0, tx], [0, 1, ty], [0, 0, scale]])
|
||||
K = self.get_camera_matrix()
|
||||
return K @ Rx @ Ry @ Rz @ T @ np.linalg.inv(K)
|
||||
|
||||
def get_camera_matrix(self) -> np.ndarray:
|
||||
w, h = config["image_size"]
|
||||
return np.array([[w / 2, 0, w / 2], [0, h / 2, h / 2], [0, 0, 1]])
|
||||
|
||||
def _apply_augmentation(
|
||||
self,
|
||||
google_img: Image.Image,
|
||||
yandex_img: Image.Image,
|
||||
matrices: Tuple[np.ndarray, np.ndarray, np.ndarray],
|
||||
) -> Tuple[Image.Image, Image.Image, np.ndarray]:
|
||||
combined_homography = matrices[2]
|
||||
yandex_aug = self._apply_homography_to_image(yandex_img, matrices[0])
|
||||
google_aug = self._apply_homography_to_image(google_img, matrices[1])
|
||||
print("F", combined_homography, np.linalg.inv(matrices[0]) @ matrices[1])
|
||||
return google_aug, yandex_aug, combined_homography
|
||||
|
||||
def _apply_homography_to_image(
|
||||
self, img: Image.Image, homography: np.ndarray
|
||||
) -> Image.Image:
|
||||
img_np = np.array(img)
|
||||
h, w = img_np.shape[:2]
|
||||
transformed = cv2.warpPerspective(
|
||||
img_np, homography, (w, h), flags=cv2.INTER_LINEAR
|
||||
)
|
||||
return Image.fromarray(transformed)
|
||||
|
||||
|
||||
def create_data_loaders(
|
||||
root_dir: str,
|
||||
batch_size: int = 32,
|
||||
train_split: float = 0.8,
|
||||
num_workers: int = 4,
|
||||
image_size: Tuple[int, int] = (256, 256),
|
||||
augment_train: bool = True,
|
||||
augment_val: bool = False,
|
||||
device=None,
|
||||
) -> Tuple[DataLoader, DataLoader]:
|
||||
transform = transforms.Compose([
|
||||
transforms.ToTensor(),
|
||||
# transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
|
||||
])
|
||||
|
||||
full_dataset = YaGoDataset(
|
||||
root_dir=root_dir,
|
||||
transform=transform,
|
||||
augment=False,
|
||||
image_size=image_size,
|
||||
cache_homographies=True,
|
||||
device=device,
|
||||
)
|
||||
|
||||
aug_dataset = YaGoDataset(
|
||||
root_dir=root_dir,
|
||||
transform=transform,
|
||||
augment=True,
|
||||
image_size=image_size,
|
||||
cache_homographies=False,
|
||||
device=device,
|
||||
)
|
||||
|
||||
dataset_size = len(full_dataset)
|
||||
train_size = int(train_split * dataset_size)
|
||||
val_size = dataset_size - train_size
|
||||
|
||||
indices = list(range(dataset_size))
|
||||
random.shuffle(indices)
|
||||
train_indices = indices[:train_size]
|
||||
val_indices = indices[train_size:]
|
||||
|
||||
train_dataset = Subset(full_dataset, train_indices)
|
||||
val_dataset = Subset(full_dataset, val_indices)
|
||||
|
||||
if augment_train:
|
||||
train_dataset = Subset(aug_dataset, train_indices)
|
||||
|
||||
train_loader = DataLoader(
|
||||
train_dataset,
|
||||
batch_size=batch_size,
|
||||
shuffle=True,
|
||||
num_workers=num_workers,
|
||||
pin_memory=True,
|
||||
)
|
||||
|
||||
val_loader = DataLoader(
|
||||
val_dataset,
|
||||
batch_size=batch_size,
|
||||
shuffle=False,
|
||||
num_workers=num_workers,
|
||||
pin_memory=True,
|
||||
)
|
||||
|
||||
return train_loader, val_loader
|
||||
|
||||
|
||||
|
||||
# Example usage
|
||||
dataset = YaGoDataset(
|
||||
root_dir=config["data_dir"],
|
||||
augment=True,
|
||||
image_size=(256, 256),
|
||||
)
|
||||
|
||||
print(f"Dataset size: {len(dataset)}")
|
||||
|
||||
# Get a sample
|
||||
sample = dataset[0]
|
||||
print(f"Sample keys: {list(sample.keys())}")
|
||||
print(f"Google image shape: {sample['google_img'].shape}")
|
||||
print(f"Yandex image shape: {sample['yandex_img'].shape}")
|
||||
print(f"Homography shape: {sample['homography'].shape}")
|
||||
|
||||
# Create data loaders
|
||||
train_loader, val_loader = create_data_loaders(
|
||||
root_dir=config["data_dir"],
|
||||
batch_size=16,
|
||||
train_split=0.8,
|
||||
)
|
||||
|
||||
print(f"Train batches: {len(train_loader)}")
|
||||
print(f"Val batches: {len(val_loader)}")
|
||||
219
models/SiaN-similarity/model.py
Normal file
219
models/SiaN-similarity/model.py
Normal file
@@ -0,0 +1,219 @@
|
||||
from typing import Tuple
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from torchvision import models
|
||||
|
||||
|
||||
class SimilarityCNN(nn.Module):
|
||||
"""
|
||||
Модель для оценки схожести двух изображений на базе предобученного бэкбона.
|
||||
|
||||
Интерфейс совместим с исходной:
|
||||
- forward(img1, img2) -> тензор (B, 1) со скором в [0, 1]
|
||||
- predict_similarity(img1, img2) -> тензор (B, 1) без градиентов
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_channels: int = 3,
|
||||
backbone_name: str = "resnet18",
|
||||
pretrained: bool = True,
|
||||
dropout_rate: float = 0.3,
|
||||
use_batch_norm: bool = True,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.input_channels = input_channels
|
||||
self.backbone_name = backbone_name
|
||||
self.pretrained = pretrained
|
||||
self.dropout_rate = dropout_rate
|
||||
self.use_batch_norm = use_batch_norm
|
||||
|
||||
# 1. Создаём бэкбон и берём фичи до последнего FC
|
||||
backbone = self._create_backbone(backbone_name, pretrained)
|
||||
|
||||
# Для ResNet18 выход фичей = 512
|
||||
self.feature_dim = backbone.fc.in_features
|
||||
# Заменяем classification head на Identity, чтобы получать только признаки
|
||||
backbone.fc = nn.Identity()
|
||||
self.backbone = backbone
|
||||
|
||||
# 2. Голова для сравнения двух векторов признаков
|
||||
# Вход: [f1, f2, |f1 - f2|, f1 * f2] => 4 * feature_dim
|
||||
compare_input_dim = self.feature_dim * 4
|
||||
|
||||
layers = [
|
||||
nn.Linear(compare_input_dim, 512),
|
||||
nn.BatchNorm1d(512) if use_batch_norm else nn.Identity(),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.Dropout(dropout_rate),
|
||||
|
||||
nn.Linear(512, 256),
|
||||
nn.BatchNorm1d(256) if use_batch_norm else nn.Identity(),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.Dropout(dropout_rate),
|
||||
|
||||
nn.Linear(256, 1),
|
||||
nn.Sigmoid(), # выход в [0, 1]
|
||||
]
|
||||
self.head = nn.Sequential(*layers)
|
||||
|
||||
def _create_backbone(self, name: str, pretrained: bool) -> nn.Module:
|
||||
name = name.lower()
|
||||
if name == "resnet18":
|
||||
model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1 if pretrained else None)
|
||||
elif name == "resnet34":
|
||||
model = models.resnet34(weights=models.ResNet34_Weights.IMAGENET1K_V1 if pretrained else None)
|
||||
else:
|
||||
raise ValueError(f"Unsupported backbone: {name}")
|
||||
# Если у тебя не 3 канала, можно добавить адаптер 1x1 conv перед model.conv1
|
||||
if self.input_channels != 3:
|
||||
old_conv = model.conv1
|
||||
model.conv1 = nn.Conv2d(
|
||||
self.input_channels,
|
||||
old_conv.out_channels,
|
||||
kernel_size=old_conv.kernel_size,
|
||||
stride=old_conv.stride,
|
||||
padding=old_conv.padding,
|
||||
bias=old_conv.bias is not None,
|
||||
)
|
||||
return model
|
||||
|
||||
def _extract_features(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""
|
||||
Прогоняет одно изображение через бэкбон и возвращает вектор признаков (B, feature_dim).
|
||||
Для ResNet: это эквивалентно model.forward(x), когда fc = Identity.
|
||||
"""
|
||||
return self.backbone(x) # (B, feature_dim)
|
||||
|
||||
def forward(self, img1: torch.Tensor, img2: torch.Tensor) -> torch.Tensor:
|
||||
"""
|
||||
img1, img2: (B, C, H, W) -> similarity: (B, 1)
|
||||
"""
|
||||
f1 = self._extract_features(img1) # (B, D)
|
||||
f2 = self._extract_features(img2) # (B, D)
|
||||
|
||||
# Вектор сравнения
|
||||
diff = torch.abs(f1 - f2)
|
||||
prod = f1 * f2
|
||||
combined = torch.cat([f1, f2, diff, prod], dim=1) # (B, 4D)
|
||||
|
||||
similarity = self.head(combined) # (B, 1) в [0, 1]
|
||||
return similarity
|
||||
|
||||
def predict_similarity(self, img1: torch.Tensor, img2: torch.Tensor) -> torch.Tensor:
|
||||
"""
|
||||
Инференс без градиентов, интерфейс как у исходной модели.
|
||||
"""
|
||||
was_training = self.training
|
||||
self.eval()
|
||||
with torch.no_grad():
|
||||
sim = self.forward(img1, img2)
|
||||
if was_training:
|
||||
self.train()
|
||||
return sim
|
||||
|
||||
|
||||
class SimilarityLoss(nn.Module):
|
||||
"""
|
||||
Оставляю тот же интерфейс loss, что и в твоём коде.
|
||||
Если таргет бинарный (0/1), BCELoss подходит.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.criterion = nn.BCELoss()
|
||||
|
||||
def forward(self, pred_similarity: torch.Tensor, target_same: torch.Tensor) -> torch.Tensor:
|
||||
return self.criterion(pred_similarity, target_same)
|
||||
|
||||
def compute_metrics(
|
||||
self,
|
||||
pred_similarity: torch.Tensor,
|
||||
target_same: torch.Tensor,
|
||||
threshold: float = 0.5,
|
||||
) -> dict:
|
||||
with torch.no_grad():
|
||||
pred_binary = (pred_similarity > threshold).float()
|
||||
target_binary = (target_same > 0.5).float()
|
||||
|
||||
correct = (pred_binary == target_binary).float()
|
||||
accuracy = correct.mean().item()
|
||||
|
||||
tp = ((pred_binary == 1) & (target_binary == 1)).float().sum().item()
|
||||
fp = ((pred_binary == 1) & (target_binary == 0)).float().sum().item()
|
||||
fn = ((pred_binary == 0) & (target_binary == 1)).float().sum().item()
|
||||
tn = ((pred_binary == 0) & (target_binary == 0)).float().sum().item()
|
||||
|
||||
precision = tp / (tp + fp + 1e-8)
|
||||
recall = tp / (tp + fn + 1e-8)
|
||||
f1 = 2 * precision * recall / (precision + recall + 1e-8)
|
||||
|
||||
return {
|
||||
"accuracy": accuracy,
|
||||
"precision": precision,
|
||||
"recall": recall,
|
||||
"f1": f1,
|
||||
"mean_similarity": pred_similarity.mean().item(),
|
||||
}
|
||||
|
||||
|
||||
def create_similarity_model(
|
||||
model_type: str = "backbone",
|
||||
input_size: Tuple[int, int] = (256, 256),
|
||||
**kwargs,
|
||||
) -> nn.Module:
|
||||
"""
|
||||
Аналог вашей фабрики, но с новым типом модели.
|
||||
"""
|
||||
if model_type == "backbone":
|
||||
return SimilarityCNN(**kwargs)
|
||||
else:
|
||||
raise ValueError(f"Unknown model type: {model_type}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
print(f"Using device: {device}")
|
||||
|
||||
model = SimilarityCNN(
|
||||
input_channels=3,
|
||||
backbone_name="resnet18",
|
||||
pretrained=True,
|
||||
dropout_rate=0.3,
|
||||
use_batch_norm=True,
|
||||
).to(device)
|
||||
|
||||
print(
|
||||
f"Model created with {sum(p.numel() for p in model.parameters()):,} parameters"
|
||||
)
|
||||
|
||||
batch_size = 4
|
||||
height, width = 256, 256
|
||||
|
||||
img1 = torch.randn(batch_size, 3, height, width).to(device)
|
||||
img2 = torch.randn(batch_size, 3, height, width).to(device)
|
||||
|
||||
print("\nTesting forward pass...")
|
||||
output = model(img1, img2)
|
||||
print(f"Output shape: {output.shape}")
|
||||
print(f"Sample output: {output[0].item():.4f}")
|
||||
|
||||
print("\nTesting prediction...")
|
||||
pred = model.predict_similarity(img1, img2)
|
||||
print(f"Prediction shape: {pred.shape}")
|
||||
|
||||
print("\nTesting loss function...")
|
||||
target = torch.rand(batch_size, 1).to(device)
|
||||
loss_fn = SimilarityLoss().to(device)
|
||||
loss = loss_fn(output, target)
|
||||
print(f"Loss value: {loss.item():.6f}")
|
||||
|
||||
print("\nTesting metrics...")
|
||||
metrics = loss_fn.compute_metrics(output, target)
|
||||
for key, value in metrics.items():
|
||||
print(f"{key}: {value:.6f}")
|
||||
|
||||
print("\nAll tests completed successfully!")
|
||||
146
models/SiaN-similarity/predict.py
Normal file
146
models/SiaN-similarity/predict.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
Script for predicting similarity between two images.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
from model import SimilarityCNN
|
||||
from PIL import Image
|
||||
from torchvision import transforms
|
||||
|
||||
|
||||
def load_image(image_path: str, image_size: tuple = (256, 256)) -> torch.Tensor:
|
||||
"""Load and preprocess image."""
|
||||
transform = transforms.Compose(
|
||||
[
|
||||
transforms.Resize(image_size),
|
||||
transforms.ToTensor(),
|
||||
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
|
||||
]
|
||||
)
|
||||
|
||||
image = Image.open(image_path).convert("RGB")
|
||||
return transform(image).unsqueeze(0) # Add batch dimension
|
||||
|
||||
|
||||
def predict_similarity(
|
||||
model: SimilarityCNN,
|
||||
image1_path: str,
|
||||
image2_path: str,
|
||||
device: torch.device,
|
||||
image_size: tuple = (256, 256),
|
||||
) -> float:
|
||||
"""Predict similarity between two images."""
|
||||
model.eval()
|
||||
|
||||
img1 = load_image(image1_path, image_size).to(device)
|
||||
img2 = load_image(image2_path, image_size).to(device)
|
||||
|
||||
with torch.no_grad():
|
||||
similarity = model(img1, img2)
|
||||
|
||||
return similarity.item()
|
||||
|
||||
|
||||
def load_model(
|
||||
checkpoint_path: str,
|
||||
device: torch.device,
|
||||
**model_kwargs,
|
||||
) -> SimilarityCNN:
|
||||
"""Load model from checkpoint."""
|
||||
model = SimilarityCNN(**model_kwargs).to(device)
|
||||
|
||||
checkpoint = torch.load(checkpoint_path, map_location=device)
|
||||
model.load_state_dict(checkpoint["model_state_dict"])
|
||||
|
||||
return model
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Predict similarity between two images"
|
||||
)
|
||||
parser.add_argument("--image1", type=str, required=True, help="Path to first image")
|
||||
parser.add_argument(
|
||||
"--image2", type=str, required=True, help="Path to second image"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--checkpoint",
|
||||
type=str,
|
||||
default="runs/similarity/checkpoints/best_model.pt",
|
||||
help="Path to model checkpoint",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--device",
|
||||
type=str,
|
||||
default="cuda" if torch.cuda.is_available() else "cpu",
|
||||
help="Device to use for inference",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--image_size",
|
||||
type=int,
|
||||
default=256,
|
||||
help="Image size for model input",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
device = torch.device(args.device)
|
||||
print(f"Using device: {device}")
|
||||
|
||||
if not os.path.exists(args.image1):
|
||||
print(f"Error: Image not found: {args.image1}")
|
||||
return
|
||||
|
||||
if not os.path.exists(args.image2):
|
||||
print(f"Error: Image not found: {args.image2}")
|
||||
return
|
||||
|
||||
if not os.path.exists(args.checkpoint):
|
||||
print(f"Warning: Checkpoint not found: {args.checkpoint}")
|
||||
print("Using randomly initialized model for demonstration")
|
||||
model = SimilarityCNN(
|
||||
input_channels=3,
|
||||
hidden_channels=64,
|
||||
num_blocks=4,
|
||||
dropout_rate=0.3,
|
||||
use_batch_norm=True,
|
||||
).to(device)
|
||||
else:
|
||||
print(f"Loading model from: {args.checkpoint}")
|
||||
model = load_model(
|
||||
checkpoint_path=args.checkpoint,
|
||||
device=device,
|
||||
input_channels=3,
|
||||
hidden_channels=64,
|
||||
num_blocks=4,
|
||||
dropout_rate=0.3,
|
||||
use_batch_norm=True,
|
||||
)
|
||||
|
||||
print(
|
||||
f"Model loaded with {sum(p.numel() for p in model.parameters()):,} parameters"
|
||||
)
|
||||
|
||||
similarity = predict_similarity(
|
||||
model=model,
|
||||
image1_path=args.image1,
|
||||
image2_path=args.image2,
|
||||
device=device,
|
||||
image_size=(args.image_size, args.image_size),
|
||||
)
|
||||
|
||||
print(f"\nSimilarity between images:")
|
||||
print(f" Image 1: {args.image1}")
|
||||
print(f" Image 2: {args.image2}")
|
||||
print(f" Similarity score: {similarity:.4f}")
|
||||
print(f" Interpretation: {'Similar' if similarity > 0.5 else 'Different'}")
|
||||
|
||||
return similarity
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2034
models/SiaN-similarity/similarity.ipynb
Normal file
2034
models/SiaN-similarity/similarity.ipynb
Normal file
File diff suppressed because one or more lines are too long
248
models/SiaN-similarity/train.py
Normal file
248
models/SiaN-similarity/train.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""
|
||||
Training script for image similarity estimation.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
from dataloader import config, create_data_loaders
|
||||
from model import SimilarityCNN, SimilarityLoss, create_similarity_model
|
||||
from torch.utils.data import DataLoader
|
||||
from torch.utils.tensorboard import SummaryWriter
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
class SimilarityTrainer:
|
||||
def __init__(
|
||||
self,
|
||||
model: nn.Module,
|
||||
train_loader: DataLoader,
|
||||
val_loader: DataLoader,
|
||||
device: torch.device,
|
||||
config: dict,
|
||||
):
|
||||
self.model = model.to(device)
|
||||
self.train_loader = train_loader
|
||||
self.val_loader = val_loader
|
||||
self.device = device
|
||||
self.config = config
|
||||
|
||||
self.criterion = SimilarityLoss()
|
||||
self.optimizer = optim.Adam(
|
||||
model.parameters(),
|
||||
lr=config.get("learning_rate", 2e-4),
|
||||
betas=(config.get("beta1", 0.5), config.get("beta2", 0.999)),
|
||||
)
|
||||
|
||||
self.writer = None
|
||||
self.best_val_loss = float("inf")
|
||||
self.epochs_without_improvement = 0
|
||||
|
||||
def train_epoch(self, epoch: int) -> dict:
|
||||
self.model.train()
|
||||
total_loss = 0
|
||||
total_samples = 0
|
||||
|
||||
pbar = tqdm(self.train_loader, desc=f"Epoch {epoch}")
|
||||
for batch_idx, batch in enumerate(pbar):
|
||||
google_img = batch["google_img"].to(self.device)
|
||||
yandex_img = batch["yandex_img"].to(self.device)
|
||||
target = batch["same_domain"].float().to(self.device).unsqueeze(1)
|
||||
|
||||
self.optimizer.zero_grad()
|
||||
|
||||
output = self.model(google_img, yandex_img)
|
||||
loss = self.criterion(output, target)
|
||||
|
||||
loss.backward()
|
||||
self.optimizer.step()
|
||||
|
||||
total_loss += loss.item() * google_img.size(0)
|
||||
total_samples += google_img.size(0)
|
||||
|
||||
if batch_idx % self.config.get("log_interval", 10) == 0:
|
||||
metrics = self.criterion.compute_metrics(output, target)
|
||||
pbar.set_postfix(
|
||||
{
|
||||
"loss": loss.item(),
|
||||
"acc": metrics["accuracy"],
|
||||
}
|
||||
)
|
||||
|
||||
if self.writer:
|
||||
self.writer.add_scalar(
|
||||
"train/loss",
|
||||
loss.item(),
|
||||
epoch * len(self.train_loader) + batch_idx,
|
||||
)
|
||||
self.writer.add_scalar(
|
||||
"train/accuracy",
|
||||
metrics["accuracy"],
|
||||
epoch * len(self.train_loader) + batch_idx,
|
||||
)
|
||||
|
||||
avg_loss = total_loss / total_samples
|
||||
return {"loss": avg_loss}
|
||||
|
||||
def validate(self) -> dict:
|
||||
self.model.eval()
|
||||
total_loss = 0
|
||||
total_samples = 0
|
||||
all_metrics = []
|
||||
|
||||
with torch.no_grad():
|
||||
for batch in tqdm(self.val_loader, desc="Validation"):
|
||||
google_img = batch["google_img"].to(self.device)
|
||||
yandex_img = batch["yandex_img"].to(self.device)
|
||||
target = batch["same_domain"].float().to(self.device).unsqueeze(1)
|
||||
|
||||
output = self.model(google_img, yandex_img)
|
||||
loss = self.criterion(output, target)
|
||||
|
||||
total_loss += loss.item() * google_img.size(0)
|
||||
total_samples += google_img.size(0)
|
||||
|
||||
metrics = self.criterion.compute_metrics(output, target)
|
||||
all_metrics.append(metrics)
|
||||
|
||||
avg_loss = total_loss / total_samples
|
||||
|
||||
avg_metrics = {}
|
||||
for key in all_metrics[0].keys():
|
||||
avg_metrics[key] = sum(m[key] for m in all_metrics) / len(all_metrics)
|
||||
|
||||
return {"loss": avg_loss, **avg_metrics}
|
||||
|
||||
def train(self, num_epochs: int):
|
||||
log_dir = self.config.get("output_dir", "runs/similarity")
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
self.writer = SummaryWriter(log_dir)
|
||||
|
||||
print(f"Starting training for {num_epochs} epochs")
|
||||
print(f"Logging to: {log_dir}")
|
||||
|
||||
for epoch in range(1, num_epochs + 1):
|
||||
print(f"\nEpoch {epoch}/{num_epochs}")
|
||||
|
||||
train_metrics = self.train_epoch(epoch)
|
||||
val_metrics = self.validate()
|
||||
|
||||
print(f"Train Loss: {train_metrics['loss']:.4f}")
|
||||
print(f"Val Loss: {val_metrics['loss']:.4f}")
|
||||
print(f"Val Accuracy: {val_metrics['accuracy']:.4f}")
|
||||
print(f"Val F1: {val_metrics['f1']:.4f}")
|
||||
|
||||
if self.writer:
|
||||
self.writer.add_scalar("epoch/train_loss", train_metrics["loss"], epoch)
|
||||
self.writer.add_scalar("epoch/val_loss", val_metrics["loss"], epoch)
|
||||
self.writer.add_scalar(
|
||||
"epoch/val_accuracy", val_metrics["accuracy"], epoch
|
||||
)
|
||||
|
||||
if val_metrics["loss"] < self.best_val_loss:
|
||||
self.best_val_loss = val_metrics["loss"]
|
||||
self.epochs_without_improvement = 0
|
||||
self.save_checkpoint(epoch, val_metrics["loss"], is_best=True)
|
||||
print(f"New best model saved with val loss: {val_metrics['loss']:.4f}")
|
||||
else:
|
||||
self.epochs_without_improvement += 1
|
||||
self.save_checkpoint(epoch, val_metrics["loss"], is_best=False)
|
||||
|
||||
patience = self.config.get("early_stopping_patience", 20)
|
||||
if self.epochs_without_improvement >= patience:
|
||||
print(
|
||||
f"Early stopping triggered after {patience} epochs without improvement"
|
||||
)
|
||||
break
|
||||
|
||||
self.writer.close()
|
||||
|
||||
def save_checkpoint(self, epoch: int, val_loss: float, is_best: bool = False):
|
||||
checkpoint_dir = os.path.join(
|
||||
self.config.get("output_dir", "runs/similarity"), "checkpoints"
|
||||
)
|
||||
os.makedirs(checkpoint_dir, exist_ok=True)
|
||||
|
||||
checkpoint = {
|
||||
"epoch": epoch,
|
||||
"model_state_dict": self.model.state_dict(),
|
||||
"optimizer_state_dict": self.optimizer.state_dict(),
|
||||
"val_loss": val_loss,
|
||||
"config": self.config,
|
||||
}
|
||||
|
||||
checkpoint_path = os.path.join(checkpoint_dir, f"checkpoint_epoch_{epoch}.pt")
|
||||
torch.save(checkpoint, checkpoint_path)
|
||||
|
||||
if is_best:
|
||||
best_path = os.path.join(checkpoint_dir, "best_model.pt")
|
||||
torch.save(checkpoint, best_path)
|
||||
|
||||
def load_checkpoint(self, checkpoint_path: str):
|
||||
checkpoint = torch.load(checkpoint_path, map_location=self.device)
|
||||
self.model.load_state_dict(checkpoint["model_state_dict"])
|
||||
self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
|
||||
return checkpoint["epoch"], checkpoint["val_loss"]
|
||||
|
||||
|
||||
def main():
|
||||
# Use config from dataloader.py
|
||||
config_dict = config.copy()
|
||||
|
||||
# Ensure image_size is tuple
|
||||
if isinstance(config_dict.get("image_size"), list):
|
||||
config_dict["image_size"] = tuple(config_dict["image_size"])
|
||||
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
print(f"Using device: {device}")
|
||||
|
||||
print("Creating data loaders...")
|
||||
train_loader, val_loader = create_data_loaders(
|
||||
root_dir=config_dict["data_dir"],
|
||||
batch_size=config_dict["batch_size"],
|
||||
train_split=config_dict["train_split"],
|
||||
num_workers=config_dict["num_workers"],
|
||||
image_size=config_dict["image_size"],
|
||||
augment_train=True,
|
||||
augment_val=False,
|
||||
device=device,
|
||||
)
|
||||
|
||||
print(f"Train batches: {len(train_loader)}")
|
||||
print(f"Val batches: {len(val_loader)}")
|
||||
|
||||
print("Creating model...")
|
||||
model = create_similarity_model(
|
||||
model_type="cnn",
|
||||
input_size=config_dict["image_size"][0]
|
||||
if isinstance(config_dict["image_size"], (tuple, list))
|
||||
else config_dict["image_size"],
|
||||
input_channels=3,
|
||||
hidden_channels=64,
|
||||
num_blocks=4,
|
||||
dropout_rate=0.3,
|
||||
use_batch_norm=True,
|
||||
)
|
||||
|
||||
print(f"Model parameters: {sum(p.numel() for p in model.parameters()):,}")
|
||||
|
||||
trainer = SimilarityTrainer(
|
||||
model=model,
|
||||
train_loader=train_loader,
|
||||
val_loader=val_loader,
|
||||
device=device,
|
||||
config=config_dict,
|
||||
)
|
||||
|
||||
print("Starting training...")
|
||||
trainer.train(config_dict["epochs"])
|
||||
|
||||
print("Training completed!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
3
models/SiaN/.gitignore
vendored
Normal file
3
models/SiaN/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
runs
|
||||
*.gen.py
|
||||
*.png
|
||||
31
models/SiaN/_schema.md
Normal file
31
models/SiaN/_schema.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# SiaN Schema
|
||||
|
||||
Notebook structure definition for SiaN model.
|
||||
|
||||
---
|
||||
|
||||
## Format
|
||||
|
||||
```
|
||||
# === IMPORTS ===
|
||||
<all imports>
|
||||
|
||||
# code: ./src/file.py
|
||||
# markdown
|
||||
"""Description"""
|
||||
|
||||
# shell:
|
||||
<shell commands>
|
||||
```
|
||||
|
||||
## Directives
|
||||
|
||||
| Directive | Description |
|
||||
|----------------|------------------------------------|
|
||||
| `# code:` | Include file from src/ |
|
||||
| `# markdown` | Description block |
|
||||
| `# shell:` | Shell script cell |
|
||||
|
||||
---
|
||||
|
||||
`build.py` generates notebook from `_schema.py`.
|
||||
110
models/SiaN/_schema.py
Normal file
110
models/SiaN/_schema.py
Normal file
@@ -0,0 +1,110 @@
|
||||
# _schema.py
|
||||
|
||||
# === IMPORTS ===
|
||||
import os
|
||||
import random
|
||||
import logging
|
||||
from typing import Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
import matplotlib.pyplot as plt
|
||||
from PIL import Image
|
||||
import seaborn as sns
|
||||
from torch.utils.data import DataLoader, Dataset, Subset
|
||||
from torch.utils.tensorboard import SummaryWriter
|
||||
from torchvision import transforms, models
|
||||
from tqdm import tqdm
|
||||
|
||||
# markdown
|
||||
"""# Configuration
|
||||
|
||||
Global settings for:
|
||||
- Data paths and image parameters
|
||||
- Training hyperparameters
|
||||
- Model architecture options
|
||||
|
||||
Contains the `config` dictionary used across all modules."""
|
||||
# code: ./src/utils.py
|
||||
|
||||
# markdown
|
||||
"""## Dataset
|
||||
|
||||
Google/Yandex image pair loader with homography augmentation.
|
||||
|
||||
**Features:**
|
||||
- Loads paired images from dual camera sources
|
||||
- Applies random homography transformations
|
||||
- Supports configurable train/val split
|
||||
|
||||
**Returns:**
|
||||
- Batch dict with `google_img`, `yandex_img`, `homography_params`"""
|
||||
# code: ./src/dataloader.py
|
||||
|
||||
# code: ./src/test_dataloader.py
|
||||
|
||||
|
||||
# markdown
|
||||
"""## Model
|
||||
|
||||
`HomographyCNN6` — CNN architecture for homography estimation.
|
||||
|
||||
**Output:** 6 parameters
|
||||
- `rx, ry, rz` — rotation angles (radians)
|
||||
- `tx, ty` — translation offsets
|
||||
- `scale` — isotropic scale factor
|
||||
|
||||
**Architecture:**
|
||||
- Dual-branch CNN (Google + Yandex images)
|
||||
- Shared backbone (configurable: resnet18/34/50)
|
||||
- Fusion head with dropout regularization"""
|
||||
# code: ./src/model.py
|
||||
|
||||
# markdown
|
||||
"""## Training
|
||||
|
||||
`HomographyTrainer` — training loop with validation and checkpointing.
|
||||
|
||||
**Features:**
|
||||
- Epoch-based training with tqdm progress bar
|
||||
- Adam optimizer with configurable LR
|
||||
- Validation after each epoch
|
||||
- Best model auto-save
|
||||
- Periodic checkpoints (every N epochs via `save_every_n_epochs`)
|
||||
|
||||
**Checkpoint saving:**
|
||||
- `best_model.pt` — lowest validation loss
|
||||
- `checkpoint_epoch_N.pt` — periodic saves"""
|
||||
# code: ./src/train.py
|
||||
|
||||
# markdown
|
||||
"""## Analysis
|
||||
|
||||
Visualization and evaluation tools:
|
||||
|
||||
- Training metrics plots (loss curves)
|
||||
- Prediction visualization on sample images
|
||||
- Error analysis and statistics"""
|
||||
# code: ./src/analyze.py
|
||||
|
||||
# markdown
|
||||
"""## Main Pipeline
|
||||
|
||||
Executes the full training workflow:
|
||||
1. Load dataset info
|
||||
2. Create data loaders
|
||||
3. Initialize model
|
||||
4. Train with validation
|
||||
5. Analyze and export results
|
||||
|
||||
**Outputs:**
|
||||
- Model checkpoints in `runs/checkpoints/`
|
||||
- TensorBoard logs in `runs/`
|
||||
- Analysis plots"""
|
||||
# code: ./src/main.py
|
||||
|
||||
# # shell:
|
||||
# !zip artefacts.zip runs/checkpoints/best_model.pt runs/images/ runs/events.*
|
||||
260
models/SiaN/build.py
Normal file
260
models/SiaN/build.py
Normal file
@@ -0,0 +1,260 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
def parse_schema(schema_path):
|
||||
with open(schema_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
imports = []
|
||||
items = []
|
||||
|
||||
in_imports_section = False
|
||||
lines = content.split('\n')
|
||||
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
stripped = line.strip()
|
||||
|
||||
if stripped == '# === IMPORTS ===':
|
||||
in_imports_section = True
|
||||
i += 1
|
||||
continue
|
||||
elif stripped.startswith('# ==='):
|
||||
in_imports_section = False
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if in_imports_section and stripped and not stripped.startswith('#'):
|
||||
imports.append(stripped)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if in_imports_section and stripped.startswith('#'):
|
||||
in_imports_section = False
|
||||
|
||||
if stripped.startswith('# code:'):
|
||||
match = re.search(r'# code:\s*(.+)', stripped)
|
||||
if match:
|
||||
items.append(('code', match.group(1).strip()))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if stripped.startswith('# inline:') or stripped.startswith('# # shell:'):
|
||||
directive = 'inline' if stripped.startswith('# inline:') else 'shell'
|
||||
i += 1
|
||||
code_lines = []
|
||||
while i < len(lines):
|
||||
stripped = lines[i].strip()
|
||||
if directive == 'shell':
|
||||
if stripped.startswith('# ') or stripped.startswith('# # '):
|
||||
line_content = lines[i].strip()
|
||||
if line_content.startswith('# # '):
|
||||
line_content = line_content[3:].lstrip()
|
||||
else:
|
||||
line_content = line_content[2:]
|
||||
code_lines.append(line_content)
|
||||
i += 1
|
||||
continue
|
||||
if stripped.startswith('#'):
|
||||
if stripped.startswith('# code:') or stripped.startswith('# inline:') or stripped.startswith('# # shell:') or stripped == '# markdown' or stripped.startswith('# ==='):
|
||||
break
|
||||
i += 1
|
||||
continue
|
||||
if stripped == '':
|
||||
i += 1
|
||||
continue
|
||||
code_lines.append(lines[i])
|
||||
i += 1
|
||||
if code_lines:
|
||||
items.append((directive, '\n'.join(code_lines).rstrip()))
|
||||
continue
|
||||
|
||||
if stripped == '# markdown':
|
||||
i += 1
|
||||
if i < len(lines):
|
||||
next_line = lines[i].strip()
|
||||
if next_line.startswith('"""'):
|
||||
if next_line.endswith('"""') and len(next_line) > 3:
|
||||
md_content = next_line[3:-3].strip()
|
||||
i += 1
|
||||
else:
|
||||
end_idx = None
|
||||
for j in range(i + 1, len(lines)):
|
||||
if '"""' in lines[j]:
|
||||
end_idx = j
|
||||
break
|
||||
if end_idx:
|
||||
md_content = '\n'.join(lines[i:end_idx])
|
||||
md_content = md_content.strip('"""').strip()
|
||||
i = end_idx + 1
|
||||
else:
|
||||
md_content = ""
|
||||
i += 1
|
||||
items.append(('markdown', md_content))
|
||||
continue
|
||||
|
||||
i += 1
|
||||
|
||||
return imports, items
|
||||
|
||||
|
||||
def strip_all_imports(content):
|
||||
lines = content.split('\n')
|
||||
result_lines = []
|
||||
skip_block = False
|
||||
if_block_indent = 0
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
|
||||
if stripped.startswith('if __name__') or stripped.startswith('if __name__ =='):
|
||||
skip_block = True
|
||||
if_block_indent = len(line) - len(line.lstrip())
|
||||
continue
|
||||
|
||||
if skip_block:
|
||||
current_indent = len(line) - len(line.lstrip())
|
||||
if line.strip() == '':
|
||||
continue
|
||||
if current_indent < if_block_indent:
|
||||
skip_block = False
|
||||
elif current_indent == if_block_indent and stripped.startswith('if '):
|
||||
skip_block = True
|
||||
if_block_indent = current_indent
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
||||
if stripped.startswith('import ') or stripped.startswith('from '):
|
||||
continue
|
||||
|
||||
result_lines.append(line)
|
||||
|
||||
while result_lines and result_lines[-1].strip() == '':
|
||||
result_lines.pop()
|
||||
|
||||
return '\n'.join(result_lines)
|
||||
|
||||
|
||||
def read_src_file(ref, base_dir):
|
||||
ref_path = ref.replace('./src/', '').replace('src/', '')
|
||||
full_path = os.path.join(base_dir, 'src', ref_path.replace('./src/', '').lstrip('/'))
|
||||
|
||||
if not full_path.endswith('.py'):
|
||||
full_path += '.py'
|
||||
|
||||
with open(full_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def build_notebook(schema_path, output_path=None):
|
||||
schema_dir = os.path.dirname(os.path.abspath(schema_path))
|
||||
if output_path is None:
|
||||
output_path = os.path.join(schema_dir, 'notebook.gen.ipynb')
|
||||
|
||||
imports, items = parse_schema(schema_path)
|
||||
|
||||
cells = []
|
||||
|
||||
if imports:
|
||||
imports_cell = {
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [imp + "\n" for imp in imports]
|
||||
}
|
||||
cells.append(imports_cell)
|
||||
|
||||
for item_type, item_content in items:
|
||||
if item_type == 'markdown':
|
||||
md_cell = {
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": item_content + "\n"
|
||||
}
|
||||
cells.append(md_cell)
|
||||
elif item_type == 'inline':
|
||||
lines = item_content.split('\n')
|
||||
while lines and lines[-1].strip() == '':
|
||||
lines.pop()
|
||||
if lines:
|
||||
lines.append('')
|
||||
cell = {
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [line + "\n" for line in lines]
|
||||
}
|
||||
cells.append(cell)
|
||||
elif item_type == 'shell':
|
||||
lines = item_content.split('\n')
|
||||
while lines and lines[-1].strip() == '':
|
||||
lines.pop()
|
||||
if lines:
|
||||
lines.append('')
|
||||
cell = {
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [line + "\n" for line in lines]
|
||||
}
|
||||
cells.append(cell)
|
||||
elif item_type == 'code':
|
||||
try:
|
||||
content = read_src_file(item_content, schema_dir)
|
||||
content = strip_all_imports(content)
|
||||
|
||||
lines = content.split('\n')
|
||||
while lines and lines[-1].strip() == '':
|
||||
lines.pop()
|
||||
if lines:
|
||||
lines.append('')
|
||||
|
||||
cell = {
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [line + "\n" for line in lines]
|
||||
}
|
||||
cells.append(cell)
|
||||
except FileNotFoundError:
|
||||
print(f"Warning: Could not find: {item_content}")
|
||||
|
||||
notebook = {
|
||||
"cells": cells,
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": ".venv",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.11.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(notebook, f, indent=1, ensure_ascii=False)
|
||||
|
||||
print(f"Notebook generated: {output_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
schema_path = os.path.join(script_dir, '_schema.py')
|
||||
|
||||
if os.path.exists(schema_path):
|
||||
build_notebook(schema_path)
|
||||
else:
|
||||
print(f"Error: _schema.py not found at {schema_path}")
|
||||
1212
models/SiaN/notebook.gen.ipynb
Normal file
1212
models/SiaN/notebook.gen.ipynb
Normal file
File diff suppressed because it is too large
Load Diff
314
models/SiaN/src/analyze.py
Normal file
314
models/SiaN/src/analyze.py
Normal file
@@ -0,0 +1,314 @@
|
||||
import os
|
||||
import torch
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
|
||||
from .dataloader import create_data_loaders
|
||||
from .model import angular_difference
|
||||
from .utils import config
|
||||
|
||||
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.2)
|
||||
|
||||
IMG_DIR = os.path.join(config["output_dir"], "images")
|
||||
os.makedirs(IMG_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def analyze_training(trainer):
|
||||
print("=== Training Analysis ===\n")
|
||||
|
||||
if trainer.writer:
|
||||
print("TensorBoard logs available at:", trainer.writer.log_dir)
|
||||
|
||||
print(f"\nBest val loss: {trainer.best_val_loss:.4f}")
|
||||
|
||||
best_model_path = os.path.join(config["output_dir"], "checkpoints", "best_model.pt")
|
||||
if os.path.exists(best_model_path):
|
||||
checkpoint = torch.load(best_model_path, map_location=trainer.device)
|
||||
trainer.model.load_state_dict(checkpoint["model_state_dict"])
|
||||
print(f"\nLoaded best model from epoch {checkpoint['epoch']} (val loss: {checkpoint['val_loss']:.4f})")
|
||||
|
||||
trainer.model.eval()
|
||||
|
||||
n_samples = 50
|
||||
names = ["tx", "ty", "rx", "ry", "rz", "scale"]
|
||||
|
||||
_, val_loader_for_analysis = create_data_loaders(
|
||||
root_dir=config["data_dir"],
|
||||
batch_size=config["batch_size"],
|
||||
train_split=config["train_split"],
|
||||
num_workers=config["num_workers"],
|
||||
image_size=config["image_size"],
|
||||
augment_train=True,
|
||||
cache_level=0,
|
||||
)
|
||||
|
||||
with torch.no_grad():
|
||||
all_errors = [[] for _ in range(6)]
|
||||
all_targets = [[] for _ in range(6)]
|
||||
all_preds = [[] for _ in range(6)]
|
||||
|
||||
sample_count = 0
|
||||
for batch in val_loader_for_analysis:
|
||||
if sample_count >= n_samples:
|
||||
break
|
||||
|
||||
google_img = batch["google_img"].to(trainer.device)
|
||||
yandex_img = batch["yandex_img"].to(trainer.device)
|
||||
target_params = batch["homography_params"].to(trainer.device)
|
||||
pred_params = trainer.model(google_img, yandex_img)
|
||||
decoded_pred = trainer.model.decode_output(pred_params)
|
||||
|
||||
batch_size = google_img.size(0)
|
||||
for i in range(batch_size):
|
||||
if sample_count >= n_samples:
|
||||
break
|
||||
|
||||
tx_error = torch.abs(decoded_pred[i, 0] - target_params[i, 0]).item()
|
||||
ty_error = torch.abs(decoded_pred[i, 1] - target_params[i, 1]).item()
|
||||
rx_error = angular_difference(decoded_pred[i, 2], target_params[i, 2]).item()
|
||||
ry_error = angular_difference(decoded_pred[i, 3], target_params[i, 3]).item()
|
||||
rz_error = angular_difference(decoded_pred[i, 4], target_params[i, 4]).item()
|
||||
scale_error = torch.abs(decoded_pred[i, 5] - target_params[i, 5]).item()
|
||||
|
||||
errors = [tx_error, ty_error, rx_error, ry_error, rz_error, scale_error]
|
||||
target_reordered = target_params[i].cpu().numpy()
|
||||
pred_reordered = decoded_pred[i].cpu().numpy()
|
||||
|
||||
for j in range(6):
|
||||
all_errors[j].append(errors[j])
|
||||
all_targets[j].append(target_reordered[j])
|
||||
all_preds[j].append(pred_reordered[j])
|
||||
|
||||
sample_count += 1
|
||||
|
||||
mean_errors = [np.mean(all_errors[i]) for i in range(6)]
|
||||
std_errors = [np.std(all_errors[i]) for i in range(6)]
|
||||
|
||||
angle_errors_deg = [np.degrees(mean_errors[i]) for i in range(2, 5)]
|
||||
|
||||
all_targets_stacked = [np.array(all_targets[i]) for i in range(6)]
|
||||
target_ranges = [np.ptp(all_targets_stacked[i]) for i in range(6)]
|
||||
relative_errors = [mean_errors[i] / target_ranges[i] if target_ranges[i] > 1e-8 else 0 for i in range(6)]
|
||||
|
||||
if len(trainer.train_losses) > 0:
|
||||
epochs = range(1, len(trainer.train_losses) + 1)
|
||||
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
|
||||
|
||||
axes[0, 0].plot(epochs, trainer.train_losses, color="#2ecc71", linewidth=2, label="Train Loss")
|
||||
axes[0, 0].plot(epochs, trainer.val_losses, color="#e74c3c", linewidth=2, label="Val Loss")
|
||||
axes[0, 0].set_xlabel("Epoch")
|
||||
axes[0, 0].set_ylabel("Loss")
|
||||
axes[0, 0].set_title("Training & Validation Loss", fontweight="bold")
|
||||
axes[0, 0].legend(framealpha=0.9)
|
||||
axes[0, 0].grid(True, alpha=0.3)
|
||||
|
||||
axes[0, 1].plot(epochs, trainer.val_losses, color="#e74c3c", linewidth=2, label="Val Loss")
|
||||
axes[0, 1].set_xlabel("Epoch")
|
||||
axes[0, 1].set_ylabel("Loss")
|
||||
axes[0, 1].set_title("Validation Loss", fontweight="bold")
|
||||
axes[0, 1].legend(framealpha=0.9)
|
||||
axes[0, 1].grid(True, alpha=0.3)
|
||||
|
||||
axes[1, 0].plot(epochs, trainer.val_mse_trans, color="#3498db", linewidth=2, label="Translation (tx, ty)")
|
||||
axes[1, 0].plot(epochs, trainer.val_mse_angle, color="#9b59b6", linewidth=2, label="Angle (rx, ry, rz)")
|
||||
axes[1, 0].plot(epochs, trainer.val_mse_scale, color="#e67e22", linewidth=2, label="Scale")
|
||||
axes[1, 0].set_xlabel("Epoch")
|
||||
axes[1, 0].set_ylabel("MSE")
|
||||
axes[1, 0].set_title("Validation MSE by Category", fontweight="bold")
|
||||
axes[1, 0].legend(framealpha=0.9)
|
||||
axes[1, 0].grid(True, alpha=0.3)
|
||||
|
||||
x_pos = np.arange(6)
|
||||
colors = ["#3498db", "#e74c3c", "#9b59b6", "#2ecc71", "#f39c12", "#1abc9c"]
|
||||
bars = axes[1, 1].bar(x_pos, mean_errors, yerr=std_errors, capsize=6, color=colors, alpha=0.85, edgecolor="white", linewidth=1.5)
|
||||
axes[1, 1].set_xticks(x_pos)
|
||||
axes[1, 1].set_xticklabels(names)
|
||||
axes[1, 1].set_ylabel("Mean Absolute Error")
|
||||
axes[1, 1].set_title(f"Mean Absolute Error per Parameter ({n_samples} samples)", fontweight="bold")
|
||||
axes[1, 1].grid(True, alpha=0.3, axis="y")
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(os.path.join(IMG_DIR, "training_loss_plots.png"), dpi=150, bbox_inches="tight")
|
||||
print("Saved training_loss_plots.png")
|
||||
plt.show()
|
||||
|
||||
fig, axes = plt.subplots(2, 3, figsize=(18, 10))
|
||||
colors = ["#3498db", "#e74c3c", "#9b59b6", "#2ecc71", "#f39c12", "#1abc9c"]
|
||||
for j in range(6):
|
||||
row = j // 3
|
||||
col = j % 3
|
||||
axes[row, col].bar(range(len(all_errors[j])), all_errors[j], color=colors[j], alpha=0.75)
|
||||
axes[row, col].set_xlabel("Sample", fontsize=10)
|
||||
axes[row, col].set_ylabel("Absolute Error", fontsize=10)
|
||||
axes[row, col].set_title(f"{names[j]}: Mean={np.mean(all_errors[j]):.4f}, Std={np.std(all_errors[j]):.4f}", fontweight="bold", fontsize=11)
|
||||
axes[row, col].grid(True, alpha=0.3, axis="y")
|
||||
plt.suptitle(f"Mean Absolute Error per Parameter ({n_samples} samples)", fontsize=14, fontweight="bold")
|
||||
plt.tight_layout()
|
||||
plt.savefig(os.path.join(IMG_DIR, "mae_per_parameter.png"), dpi=150, bbox_inches="tight")
|
||||
print("Saved mae_per_parameter.png")
|
||||
plt.show()
|
||||
|
||||
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
|
||||
|
||||
x_pos = np.arange(6)
|
||||
colors = ["#3498db", "#e74c3c", "#9b59b6", "#2ecc71", "#f39c12", "#1abc9c"]
|
||||
bars = axes[0].bar(x_pos, mean_errors, yerr=std_errors, capsize=6, color=colors, alpha=0.85, edgecolor="white", linewidth=1.5)
|
||||
axes[0].set_xticks(x_pos)
|
||||
axes[0].set_xticklabels(names)
|
||||
axes[0].set_ylabel("Mean Absolute Error")
|
||||
axes[0].set_title("Mean Absolute Error per Parameter (with std)", fontweight="bold")
|
||||
axes[0].grid(True, alpha=0.3, axis="y")
|
||||
|
||||
bp = axes[1].boxplot([all_errors[i] for i in range(6)], labels=names, patch_artist=True)
|
||||
for patch, color in zip(bp["boxes"], colors):
|
||||
patch.set_facecolor(color)
|
||||
patch.set_alpha(0.8)
|
||||
axes[1].set_ylabel("Absolute Error")
|
||||
axes[1].set_title(f"Error Distribution per Parameter ({n_samples} samples)", fontweight="bold")
|
||||
axes[1].grid(True, alpha=0.3, axis="y")
|
||||
|
||||
rel_err_pos = np.arange(6)
|
||||
bars = axes[2].bar(rel_err_pos, relative_errors, color=colors, alpha=0.85, edgecolor="white", linewidth=1.5)
|
||||
axes[2].set_xticks(rel_err_pos)
|
||||
axes[2].set_xticklabels(names)
|
||||
axes[2].set_ylabel("Relative Error (MAE / Range)")
|
||||
axes[2].set_title("Relative Error per Parameter", fontweight="bold")
|
||||
axes[2].grid(True, alpha=0.3, axis="y")
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(os.path.join(IMG_DIR, "mae_boxplot.png"), dpi=150, bbox_inches="tight")
|
||||
print("Saved mae_boxplot.png")
|
||||
plt.show()
|
||||
|
||||
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
|
||||
|
||||
angle_names = ["rx", "ry", "rz"]
|
||||
x_pos = np.arange(3)
|
||||
colors_angle = ["#9b59b6", "#2ecc71", "#f39c12"]
|
||||
bars = axes[0].bar(x_pos, angle_errors_deg, color=colors_angle, alpha=0.85, edgecolor="white", linewidth=1.5)
|
||||
axes[0].set_xticks(x_pos)
|
||||
axes[0].set_xticklabels(angle_names)
|
||||
axes[0].set_ylabel("Mean Absolute Error (degrees)")
|
||||
axes[0].set_title("Angle MAE in Degrees", fontweight="bold")
|
||||
axes[0].grid(True, alpha=0.3, axis="y")
|
||||
for i, e in enumerate(angle_errors_deg):
|
||||
axes[0].text(i, e + 0.5, f"{e:.1f}°", ha="center", va="bottom", fontsize=11, fontweight="bold")
|
||||
|
||||
trans_scale_errs = [mean_errors[0], mean_errors[1], mean_errors[5]]
|
||||
trans_scale_names = ["tx", "ty", "scale"]
|
||||
x_pos = np.arange(3)
|
||||
colors_trans = ["#3498db", "#e74c3c", "#1abc9c"]
|
||||
bars = axes[1].bar(x_pos, trans_scale_errs, color=colors_trans, alpha=0.85, edgecolor="white", linewidth=1.5)
|
||||
axes[1].set_xticks(x_pos)
|
||||
axes[1].set_xticklabels(trans_scale_names)
|
||||
axes[1].set_ylabel("Mean Absolute Error")
|
||||
axes[1].set_title("Translation & Scale MAE", fontweight="bold")
|
||||
axes[1].grid(True, alpha=0.3, axis="y")
|
||||
for i, e in enumerate(trans_scale_errs):
|
||||
axes[1].text(i, e + 0.01, f"{e:.4f}", ha="center", va="bottom", fontsize=11, fontweight="bold")
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(os.path.join(IMG_DIR, "mae_by_category.png"), dpi=150, bbox_inches="tight")
|
||||
print("Saved mae_by_category.png")
|
||||
plt.show()
|
||||
|
||||
print("\n=== Sample Predictions (20 pairs) ===")
|
||||
n_vis_samples = 20
|
||||
|
||||
with torch.no_grad():
|
||||
vis_count = 0
|
||||
for batch in val_loader_for_analysis:
|
||||
if vis_count >= n_vis_samples:
|
||||
break
|
||||
batch_size = batch["google_img"].size(0)
|
||||
|
||||
for i in range(batch_size):
|
||||
if vis_count >= n_vis_samples:
|
||||
break
|
||||
|
||||
google_img = batch["google_img"][i:i+1].to(trainer.device)
|
||||
yandex_img = batch["yandex_img"][i:i+1].to(trainer.device)
|
||||
target_params = batch["homography_params"][i:i+1].to(trainer.device)
|
||||
pred_params = trainer.model(google_img, yandex_img)
|
||||
decoded_pred = trainer.model.decode_output(pred_params)
|
||||
|
||||
tx_error = torch.abs(decoded_pred[0, 0] - target_params[0, 0]).item()
|
||||
ty_error = torch.abs(decoded_pred[0, 1] - target_params[0, 1]).item()
|
||||
rx_error = angular_difference(decoded_pred[0, 2], target_params[0, 2]).item()
|
||||
ry_error = angular_difference(decoded_pred[0, 3], target_params[0, 3]).item()
|
||||
rz_error = angular_difference(decoded_pred[0, 4], target_params[0, 4]).item()
|
||||
scale_error = torch.abs(decoded_pred[0, 5] - target_params[0, 5]).item()
|
||||
|
||||
errors = np.array([tx_error, ty_error, rx_error, ry_error, rz_error, scale_error])
|
||||
targets = target_params[0].cpu().numpy()
|
||||
preds = decoded_pred[0].cpu().numpy()
|
||||
|
||||
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
|
||||
|
||||
axes[0, 0].imshow(google_img[0].cpu().permute(1, 2, 0))
|
||||
axes[0, 0].set_title("Google Image", fontweight="bold", fontsize=12)
|
||||
axes[0, 0].axis("off")
|
||||
|
||||
axes[0, 1].imshow(yandex_img[0].cpu().permute(1, 2, 0))
|
||||
axes[0, 1].set_title("Yandex Image", fontweight="bold", fontsize=12)
|
||||
axes[0, 1].axis("off")
|
||||
|
||||
x_pos = np.arange(6)
|
||||
width = 0.35
|
||||
axes[1, 0].bar(x_pos - width/2, targets, width, label="Target", color="#3498db", alpha=0.85)
|
||||
axes[1, 0].bar(x_pos + width/2, preds, width, label="Predicted", color="#e74c3c", alpha=0.85)
|
||||
axes[1, 0].set_xticks(x_pos)
|
||||
axes[1, 0].set_xticklabels(names)
|
||||
axes[1, 0].set_ylabel("Parameter Value")
|
||||
axes[1, 0].set_title("Target vs Predicted", fontweight="bold", fontsize=12)
|
||||
axes[1, 0].legend(framealpha=0.9)
|
||||
axes[1, 0].grid(True, alpha=0.3, axis="y")
|
||||
|
||||
colors = ["#3498db", "#e74c3c", "#9b59b6", "#2ecc71", "#f39c12", "#1abc9c"]
|
||||
bars = axes[1, 1].bar(x_pos, errors, color=colors, alpha=0.85, edgecolor="white", linewidth=1.2)
|
||||
axes[1, 1].set_xticks(x_pos)
|
||||
axes[1, 1].set_xticklabels(names)
|
||||
axes[1, 1].set_ylabel("Absolute Error")
|
||||
axes[1, 1].set_title(f"Prediction Error (Mean: {np.mean(errors):.4f})", fontweight="bold", fontsize=12)
|
||||
axes[1, 1].grid(True, alpha=0.3, axis="y")
|
||||
for i_e, e in enumerate(errors):
|
||||
axes[1, 1].text(i_e, e + 0.01, f"{e:.3f}", ha="center", va="bottom", fontsize=9)
|
||||
|
||||
plt.suptitle(f"Sample {vis_count + 1}", fontsize=14, fontweight="bold")
|
||||
plt.tight_layout()
|
||||
plt.savefig(os.path.join(IMG_DIR, f"prediction_sample_{vis_count + 1:02d}.png"), dpi=100, bbox_inches="tight")
|
||||
plt.show()
|
||||
print(f"Saved prediction_sample_{vis_count + 1:02d}.png")
|
||||
|
||||
vis_count += 1
|
||||
|
||||
print(f"\nPrediction errors over {n_samples} samples:")
|
||||
print(f"{'Param':<8} {'Mean Error':>12} {'Std Error':>12} {'Min':>8} {'Max':>8} {'Rel Err':>10}")
|
||||
print("-" * 62)
|
||||
for i in range(6):
|
||||
mean_err = np.mean(all_errors[i])
|
||||
std_err = np.std(all_errors[i])
|
||||
min_err = np.min(all_errors[i])
|
||||
max_err = np.max(all_errors[i])
|
||||
rel_err = relative_errors[i]
|
||||
print(f"{names[i]:<8} {mean_err:>12.4f} {std_err:>12.4f} {min_err:>8.4f} {max_err:>8.4f} {rel_err:>10.4f}")
|
||||
|
||||
print(f"\nAngle errors in degrees:")
|
||||
print(f"{'Param':<8} {'MAE (deg)':>12} {'MAE (rad)':>12}")
|
||||
print("-" * 35)
|
||||
for i, name in enumerate(["rx", "ry", "rz"]):
|
||||
print(f"{name:<8} {angle_errors_deg[i]:>12.2f} {mean_errors[i+2]:>12.4f}")
|
||||
|
||||
return {
|
||||
"best_val_loss": trainer.best_val_loss,
|
||||
"train_losses": trainer.train_losses,
|
||||
"val_losses": trainer.val_losses,
|
||||
"val_mse_trans": trainer.val_mse_trans,
|
||||
"val_mse_angle": trainer.val_mse_angle,
|
||||
"val_mse_scale": trainer.val_mse_scale,
|
||||
"mean_errors": mean_errors,
|
||||
"std_errors": std_errors,
|
||||
"angle_errors_deg": angle_errors_deg,
|
||||
"relative_errors": relative_errors,
|
||||
}
|
||||
142
models/SiaN/src/dataloader.py
Normal file
142
models/SiaN/src/dataloader.py
Normal file
@@ -0,0 +1,142 @@
|
||||
import os
|
||||
import random
|
||||
from typing import Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image
|
||||
from torch.utils.data import DataLoader, Dataset, Subset
|
||||
from torchvision import transforms
|
||||
|
||||
from .utils import config, get_camera_matrix, generate_random_homography_params, homography_params_to_matrix
|
||||
|
||||
|
||||
class YaGoDataset(Dataset):
|
||||
def __init__(self, root_dir: str, transform=None, augment: bool = True,
|
||||
image_size: Tuple[int, int] = (256, 256), cache_level: int = 5):
|
||||
self.root_dir = root_dir
|
||||
self.transform = transform
|
||||
self.augment = augment
|
||||
self.image_size = image_size
|
||||
self.cache_level = cache_level
|
||||
self.K = get_camera_matrix(image_size[1], image_size[0])
|
||||
self.image_pairs = self._discover_image_pairs()
|
||||
self._load_images_to_memory()
|
||||
self._init_cache()
|
||||
|
||||
def _discover_image_pairs(self):
|
||||
pairs = []
|
||||
for f in os.listdir(self.root_dir):
|
||||
if f.endswith("_google.png"):
|
||||
idx = f.split("_")[0]
|
||||
yandex_path = os.path.join(self.root_dir, f"{idx}_yandex.png")
|
||||
if os.path.exists(yandex_path):
|
||||
pairs.append({"idx": int(idx), "google": os.path.join(self.root_dir, f), "yandex": yandex_path})
|
||||
return sorted(pairs, key=lambda x: x["idx"])
|
||||
|
||||
def _load_images_to_memory(self):
|
||||
self._google_images = []
|
||||
self._yandex_images = []
|
||||
for pair in self.image_pairs:
|
||||
google_img = cv2.imread(pair["google"])
|
||||
google_img = cv2.cvtColor(google_img, cv2.COLOR_BGR2RGB)
|
||||
google_img = cv2.resize(google_img, (self.image_size[1], self.image_size[0]), interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
yandex_img = cv2.imread(pair["yandex"])
|
||||
yandex_img = cv2.cvtColor(yandex_img, cv2.COLOR_BGR2RGB)
|
||||
yandex_img = cv2.resize(yandex_img, (self.image_size[1], self.image_size[0]), interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
self._google_images.append(google_img)
|
||||
self._yandex_images.append(yandex_img)
|
||||
|
||||
def _init_cache(self):
|
||||
self._access_counts = [0] * len(self.image_pairs)
|
||||
self._cached_google = [None] * len(self.image_pairs)
|
||||
self._cached_yandex = [None] * len(self.image_pairs)
|
||||
self._cached_homography = [None] * len(self.image_pairs)
|
||||
self._cached_params = [None] * len(self.image_pairs)
|
||||
|
||||
def _generate_augmented(self, idx):
|
||||
google_img = self._google_images[idx].copy()
|
||||
yandex_img = self._yandex_images[idx].copy()
|
||||
|
||||
params1 = generate_random_homography_params()
|
||||
params2 = generate_random_homography_params()
|
||||
H1 = homography_params_to_matrix(params1, self.K)
|
||||
H2 = homography_params_to_matrix(params2, self.K)
|
||||
|
||||
yandex_warped = cv2.warpPerspective(yandex_img, H1, (self.image_size[1], self.image_size[0]))
|
||||
google_warped = cv2.warpPerspective(google_img, H2 @ H1, (self.image_size[1], self.image_size[0]))
|
||||
|
||||
return google_warped, yandex_warped, H2, params2
|
||||
|
||||
def __len__(self):
|
||||
return len(self.image_pairs)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
self._access_counts[idx] += 1
|
||||
|
||||
use_cache = self.augment and self.cache_level > 0 and self._access_counts[idx] > 1 and (self._access_counts[idx] - 1) % self.cache_level != 0
|
||||
|
||||
if use_cache:
|
||||
google_img = self._cached_google[idx]
|
||||
yandex_img = self._cached_yandex[idx]
|
||||
target_matrix = self._cached_homography[idx]
|
||||
target_params = self._cached_params[idx]
|
||||
elif self.augment:
|
||||
google_img, yandex_img, target_matrix, target_params = self._generate_augmented(idx)
|
||||
if self.cache_level > 0:
|
||||
self._cached_google[idx] = google_img
|
||||
self._cached_yandex[idx] = yandex_img
|
||||
self._cached_homography[idx] = target_matrix
|
||||
self._cached_params[idx] = target_params
|
||||
else:
|
||||
google_img = self._google_images[idx]
|
||||
yandex_img = self._yandex_images[idx]
|
||||
target_params = np.array([0, 0, 0, 0, 0, 1], dtype=np.float32)
|
||||
target_matrix = np.eye(3, dtype=np.float32)
|
||||
|
||||
google_img = Image.fromarray(google_img)
|
||||
yandex_img = Image.fromarray(yandex_img)
|
||||
|
||||
if self.transform:
|
||||
google_img = self.transform(google_img)
|
||||
yandex_img = self.transform(yandex_img)
|
||||
|
||||
return {
|
||||
"google_img": google_img,
|
||||
"yandex_img": yandex_img,
|
||||
"homography_matrix": torch.from_numpy(target_matrix).float(),
|
||||
"homography_params": torch.from_numpy(target_params).float(),
|
||||
}
|
||||
|
||||
|
||||
def create_data_loaders(root_dir, batch_size=32, train_split=0.8, num_workers=0,
|
||||
image_size=(256, 256), augment_train=True, cache_level=5):
|
||||
transform = transforms.Compose([
|
||||
transforms.ToTensor(),
|
||||
# transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
|
||||
])
|
||||
|
||||
full_ds = YaGoDataset(root_dir, transform=transform, augment=False, image_size=image_size, cache_level=cache_level)
|
||||
aug_ds = YaGoDataset(root_dir, transform=transform, augment=True, image_size=image_size, cache_level=cache_level)
|
||||
|
||||
indices = list(range(len(full_ds)))
|
||||
random.shuffle(indices)
|
||||
split = int(train_split * len(indices))
|
||||
|
||||
train_ds = Subset(aug_ds if augment_train else full_ds, indices[:split])
|
||||
val_ds = Subset(aug_ds, indices[split:])
|
||||
|
||||
return (DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True),
|
||||
DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True))
|
||||
|
||||
|
||||
def get_dataset_info():
|
||||
ds = YaGoDataset(config["data_dir"], augment=True, image_size=config["image_size"])
|
||||
return {
|
||||
"size": len(ds),
|
||||
"sample_keys": list(ds[0].keys()),
|
||||
"sample_params": ds[0]["homography_params"].numpy()
|
||||
}
|
||||
60
models/SiaN/src/main.py
Normal file
60
models/SiaN/src/main.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
import torch
|
||||
|
||||
from .dataloader import create_data_loaders, get_dataset_info
|
||||
from .model import HomographyCNN6, HomographyHybridCNN, HomographyLoss, count_parameters
|
||||
from .train import HomographyTrainer
|
||||
from .analyze import analyze_training
|
||||
from .utils import config
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger.info("=" * 50)
|
||||
logger.info("SiaN Training Pipeline")
|
||||
logger.info("=" * 50)
|
||||
|
||||
dataset_info = get_dataset_info()
|
||||
logger.info(f"Dataset: {dataset_info['size']} samples, keys={dataset_info['sample_keys']}")
|
||||
|
||||
train_loader, val_loader = create_data_loaders(
|
||||
root_dir=config["data_dir"],
|
||||
batch_size=config["batch_size"],
|
||||
train_split=config["train_split"],
|
||||
num_workers=config["num_workers"],
|
||||
image_size=config["image_size"],
|
||||
)
|
||||
logger.info(f"Data loaders created: train={len(train_loader.dataset)}, val={len(val_loader.dataset)}")
|
||||
|
||||
# model = HomographyCNN6(
|
||||
# input_channels=3,
|
||||
# backbone_name=config["backbone"],
|
||||
# pretrained=True,
|
||||
# dropout_rate=config["dropout_rate"]
|
||||
# )
|
||||
|
||||
model = HomographyHybridCNN(
|
||||
input_channels=3,
|
||||
dropout_rate=config["droupout_rate"],
|
||||
)
|
||||
|
||||
logger.info(f"Model created with {count_parameters(model):,} parameters")
|
||||
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
logger.info(f"Using device: {device}")
|
||||
|
||||
trainer = HomographyTrainer(model, train_loader, val_loader, device, HomographyLoss())
|
||||
logger.info("Starting training...")
|
||||
trainer.train(config["epochs"])
|
||||
logger.info("Training completed")
|
||||
|
||||
logger.info("Analyzing model...")
|
||||
results = analyze_training(trainer)
|
||||
logger.info(f"Analysis complete: best_val_loss={results['best_val_loss']:.4f}")
|
||||
|
||||
logger.info("=" * 50)
|
||||
logger.info("Pipeline completed successfully")
|
||||
logger.info("=" * 50)
|
||||
265
models/SiaN/src/model.py
Normal file
265
models/SiaN/src/model.py
Normal file
@@ -0,0 +1,265 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torchvision import models
|
||||
|
||||
|
||||
def angular_difference(pred_angles, target_angles):
|
||||
diff = pred_angles - target_angles
|
||||
diff = torch.atan2(torch.sin(diff), torch.cos(diff))
|
||||
return torch.abs(diff)
|
||||
|
||||
|
||||
class HomographyCNN6(nn.Module):
|
||||
def __init__(self, input_channels=3, backbone_name="resnet18", pretrained=True, dropout_rate=0.3):
|
||||
super().__init__()
|
||||
backbone = getattr(models, backbone_name)(weights=models.ResNet18_Weights.IMAGENET1K_V1 if pretrained else None)
|
||||
self.feature_dim = backbone.fc.in_features
|
||||
backbone.fc = nn.Identity()
|
||||
self.backbone = backbone
|
||||
|
||||
self.head = nn.Sequential(
|
||||
nn.Linear(self.feature_dim * 4, 1024),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.Dropout(dropout_rate),
|
||||
nn.Linear(1024, 512),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.Dropout(dropout_rate),
|
||||
nn.Linear(512, 256),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.Dropout(dropout_rate),
|
||||
nn.Linear(256, 6),
|
||||
)
|
||||
self._init_weights()
|
||||
|
||||
def _normalize_sin_cos(self, _sin, _cos):
|
||||
_len = torch.sqrt(_sin ** 2 + _cos ** 2)
|
||||
return _sin / _len, _cos / _len
|
||||
|
||||
def _init_weights(self):
|
||||
for module in self.head.modules():
|
||||
if isinstance(module, nn.Linear):
|
||||
nn.init.kaiming_normal_(module.weight, mode='fan_in', nonlinearity='relu')
|
||||
if module.bias is not None:
|
||||
nn.init.zeros_(module.bias)
|
||||
|
||||
def forward(self, img1, img2):
|
||||
f1 = self.backbone(img1)
|
||||
f2 = self.backbone(img2)
|
||||
combined = torch.cat([f1, f2, torch.abs(f1 - f2), f1 * f2], dim=1)
|
||||
|
||||
output = self.head(combined)
|
||||
|
||||
output = torch.tanh(output) # [-1; 1]
|
||||
modified = output.clone()
|
||||
modified[:, 2:6] = torch.mul(output[:, 2:6], torch.pi) # [-pi; pi]
|
||||
|
||||
return modified
|
||||
|
||||
def decode_output(self, output):
|
||||
tx = output[:, 0]
|
||||
ty = output[:, 1]
|
||||
scale = output[:, 5]
|
||||
angle1 = output[:, 2]
|
||||
angle2 = output[:, 3]
|
||||
angle3 = output[:, 4]
|
||||
|
||||
return torch.stack([tx, ty, angle1, angle2, angle3, scale], dim=1)
|
||||
|
||||
def get_components(self, output):
|
||||
decoded = self.decode_output(output)
|
||||
return {
|
||||
"tx": decoded[:, 0],
|
||||
"ty": decoded[:, 1],
|
||||
"rx": decoded[:, 2],
|
||||
"ry": decoded[:, 3],
|
||||
"rz": decoded[:, 4],
|
||||
"scale": decoded[:, 5],
|
||||
}
|
||||
|
||||
|
||||
class HomographyHybridCNN(nn.Module):
|
||||
def __init__(self, input_channels=3, use_resnet_layers=2, dropout_rate=0.3):
|
||||
super().__init__()
|
||||
|
||||
if use_resnet_layers == 1:
|
||||
resnet = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)
|
||||
self.conv1 = resnet.conv1
|
||||
self.bn1 = resnet.bn1
|
||||
self.relu = resnet.relu
|
||||
self.maxpool = resnet.maxpool
|
||||
conv_out_channels = 64
|
||||
elif use_resnet_layers == 2:
|
||||
resnet = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)
|
||||
self.conv1 = resnet.conv1
|
||||
self.bn1 = resnet.bn1
|
||||
self.relu = resnet.relu
|
||||
self.maxpool = resnet.maxpool
|
||||
self.conv2 = resnet.layer1[0].conv1
|
||||
self.bn2 = resnet.layer1[0].bn1
|
||||
self.conv2_2 = resnet.layer1[0].conv2
|
||||
self.bn2_2 = resnet.layer1[0].bn2
|
||||
self.relu2 = resnet.layer1[0].relu
|
||||
self.maxpool2 = resnet.maxpool
|
||||
conv_out_channels = 64
|
||||
else:
|
||||
raise ValueError("use_resnet_layers must be 1 or 2")
|
||||
|
||||
self.use_resnet_layers = use_resnet_layers
|
||||
self.feature_map_size = 64
|
||||
|
||||
self.conv_head = nn.Sequential(
|
||||
nn.Conv2d(conv_out_channels, 128, kernel_size=3, padding=1),
|
||||
nn.BatchNorm2d(128),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.Conv2d(128, 256, kernel_size=3, padding=1),
|
||||
nn.BatchNorm2d(256),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.MaxPool2d(2),
|
||||
)
|
||||
|
||||
self.global_pool = nn.AdaptiveAvgPool2d((1, 1))
|
||||
|
||||
feature_dim = 256 * 4
|
||||
|
||||
self.head = nn.Sequential(
|
||||
nn.Linear(feature_dim, 1024),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.Dropout(dropout_rate),
|
||||
nn.Linear(1024, 512),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.Dropout(dropout_rate),
|
||||
nn.Linear(512, 256),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.Dropout(dropout_rate),
|
||||
nn.Linear(256, 6),
|
||||
)
|
||||
self._init_weights()
|
||||
|
||||
def _init_weights(self):
|
||||
for module in self.head.modules():
|
||||
if isinstance(module, nn.Linear):
|
||||
nn.init.kaiming_normal_(module.weight, mode='fan_in', nonlinearity='relu')
|
||||
if module.bias is not None:
|
||||
nn.init.zeros_(module.bias)
|
||||
|
||||
def forward(self, img1, img2):
|
||||
x1 = self._extract_features(img1)
|
||||
x2 = self._extract_features(img2)
|
||||
|
||||
combined = torch.cat([x1, x2, torch.abs(x1 - x2), x1 * x2], dim=1)
|
||||
output = self.head(combined)
|
||||
|
||||
output = torch.tanh(output)
|
||||
modified = output.clone()
|
||||
modified[:, 2:6] = torch.mul(output[:, 2:6], torch.pi)
|
||||
|
||||
return modified
|
||||
|
||||
def _extract_features(self, x):
|
||||
x = self.conv1(x)
|
||||
x = self.bn1(x)
|
||||
x = self.relu(x)
|
||||
x = self.maxpool(x)
|
||||
|
||||
if self.use_resnet_layers >= 2:
|
||||
x = self.conv2(x)
|
||||
x = self.bn2(x)
|
||||
x = self.relu(x)
|
||||
x = self.conv2_2(x)
|
||||
x = self.bn2_2(x)
|
||||
x = self.relu2(x)
|
||||
x = self.maxpool2(x)
|
||||
|
||||
x = self.conv_head(x)
|
||||
x = self.global_pool(x)
|
||||
x = x.view(x.size(0), -1)
|
||||
|
||||
return x
|
||||
|
||||
def decode_output(self, output):
|
||||
tx = output[:, 0]
|
||||
ty = output[:, 1]
|
||||
scale = output[:, 5]
|
||||
angle1 = output[:, 2]
|
||||
angle2 = output[:, 3]
|
||||
angle3 = output[:, 4]
|
||||
return torch.stack([tx, ty, angle1, angle2, angle3, scale], dim=1)
|
||||
|
||||
def get_components(self, output):
|
||||
decoded = self.decode_output(output)
|
||||
return {
|
||||
"tx": decoded[:, 0],
|
||||
"ty": decoded[:, 1],
|
||||
"rx": decoded[:, 2],
|
||||
"ry": decoded[:, 3],
|
||||
"rz": decoded[:, 4],
|
||||
"scale": decoded[:, 5],
|
||||
}
|
||||
|
||||
|
||||
class HomographyLoss6(nn.Module):
|
||||
def __init__(self, angle_loss_weight=1.0, trans_loss_weight=1.0, scale_loss_weight=1.0):
|
||||
super().__init__()
|
||||
self.criterion = nn.MSELoss()
|
||||
self.angle_loss_weight = angle_loss_weight
|
||||
self.trans_loss_weight = trans_loss_weight
|
||||
self.scale_loss_weight = scale_loss_weight
|
||||
|
||||
@staticmethod
|
||||
def dot_angles(src, dest):
|
||||
sin_src = torch.sin(src)
|
||||
cos_src = torch.cos(src)
|
||||
sin_dest = torch.sin(dest)
|
||||
cos_dest = torch.cos(dest)
|
||||
return sin_src * sin_dest + cos_src * cos_dest
|
||||
|
||||
def forward(self, pred, target):
|
||||
tx_loss = self.criterion(pred[:, 0], target[:, 0])
|
||||
ty_loss = self.criterion(pred[:, 1], target[:, 1])
|
||||
|
||||
dot_rx = HomographyLoss6.dot_angles(pred[:, 2], target[:, 2])
|
||||
dot_ry = HomographyLoss6.dot_angles(pred[:, 3], target[:, 3])
|
||||
dot_rz = HomographyLoss6.dot_angles(pred[:, 4], target[:, 4])
|
||||
|
||||
rx_loss = self.criterion(dot_rx, torch.ones_like(dot_rx))
|
||||
ry_loss = self.criterion(dot_ry, torch.ones_like(dot_ry))
|
||||
rz_loss = self.criterion(dot_rz, torch.ones_like(dot_rz))
|
||||
|
||||
scale_loss = self.criterion(pred[:, 5], target[:, 5])
|
||||
|
||||
total_loss = (
|
||||
self.trans_loss_weight * (tx_loss + ty_loss) +
|
||||
self.angle_loss_weight * (rx_loss + ry_loss + rz_loss) +
|
||||
self.scale_loss_weight * scale_loss
|
||||
)
|
||||
|
||||
return total_loss
|
||||
|
||||
def compute_mse_components(self, decoded, target):
|
||||
tx_mse = self.criterion(decoded[:, 0], target[:, 0]).item()
|
||||
ty_mse = self.criterion(decoded[:, 1], target[:, 1]).item()
|
||||
|
||||
dot_rx = HomographyLoss6.dot_angles(decoded[:, 2], target[:, 2])
|
||||
dot_ry = HomographyLoss6.dot_angles(decoded[:, 3], target[:, 3])
|
||||
dot_rz = HomographyLoss6.dot_angles(decoded[:, 4], target[:, 4])
|
||||
|
||||
rx_mse = self.criterion(dot_rx, torch.ones_like(dot_rx)).item()
|
||||
ry_mse = self.criterion(dot_ry, torch.ones_like(dot_ry)).item()
|
||||
rz_mse = self.criterion(dot_rz, torch.ones_like(dot_rz)).item()
|
||||
|
||||
scale_mse = self.criterion(decoded[:, 5], target[:, 5]).item()
|
||||
|
||||
avg_angle_loss = (rx_mse + ry_mse + rz_mse) / 3
|
||||
|
||||
return {
|
||||
'trans': (tx_mse + ty_mse) / 2,
|
||||
'angle': avg_angle_loss,
|
||||
'scale': scale_mse
|
||||
}
|
||||
|
||||
|
||||
HomographyLoss = HomographyLoss6
|
||||
|
||||
|
||||
def count_parameters(model):
|
||||
return sum(p.numel() for p in model.parameters())
|
||||
15
models/SiaN/src/test_dataloader.py
Normal file
15
models/SiaN/src/test_dataloader.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from ..src.dataloader import *
|
||||
|
||||
train_loader, val_loader = create_data_loaders(config['data_dir'])
|
||||
batch = next(iter(train_loader))
|
||||
google_img = batch['google_img'][0]
|
||||
yandex_img = batch['yandex_img'][0]
|
||||
|
||||
# google_img.permute((1, 2, 0)) * 255
|
||||
batch['homography_params'].mean(axis=0)
|
||||
|
||||
print(batch['homography_matrix'][0])
|
||||
print(batch['homography_params'][0])
|
||||
K = get_camera_matrix(config['image_size'][0], config['image_size'][1])
|
||||
print(homography_params_to_matrix(batch['homography_params'][0], K))
|
||||
print(matrix_to_homography_params(batch['homography_matrix'][0].numpy(), K))
|
||||
122
models/SiaN/src/train.py
Normal file
122
models/SiaN/src/train.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import os
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
from torch.utils.tensorboard import SummaryWriter
|
||||
from tqdm import tqdm
|
||||
|
||||
from .dataloader import create_data_loaders
|
||||
from .model import HomographyCNN6, HomographyLoss6, count_parameters
|
||||
from .utils import config
|
||||
|
||||
|
||||
class HomographyTrainer:
|
||||
def __init__(self, model, train_loader, val_loader, device, criterion):
|
||||
self.model = model.to(device)
|
||||
self.train_loader = train_loader
|
||||
self.val_loader = val_loader
|
||||
self.device = device
|
||||
self.criterion = criterion
|
||||
self.optimizer = optim.Adam(model.parameters(), lr=config["learning_rate"], weight_decay=1e-4)
|
||||
self.writer = None
|
||||
self.best_val_loss = float("inf")
|
||||
self.train_losses = []
|
||||
self.val_losses = []
|
||||
self.train_mse_trans = []
|
||||
self.train_mse_angle = []
|
||||
self.train_mse_scale = []
|
||||
self.val_mse_trans = []
|
||||
self.val_mse_angle = []
|
||||
self.val_mse_scale = []
|
||||
|
||||
def train_epoch(self, epoch):
|
||||
self.model.train()
|
||||
total_loss, total_samples = 0, 0
|
||||
mse_trans_sum, mse_angle_sum, mse_scale_sum = 0, 0, 0
|
||||
pbar = tqdm(self.train_loader, desc=f"Epoch {epoch}")
|
||||
for batch_idx, batch in enumerate(pbar):
|
||||
google_img = batch["google_img"].to(self.device)
|
||||
yandex_img = batch["yandex_img"].to(self.device)
|
||||
target = batch["homography_params"].to(self.device)
|
||||
|
||||
self.optimizer.zero_grad()
|
||||
output = self.model(google_img, yandex_img)
|
||||
loss = self.criterion(output, target)
|
||||
loss.backward()
|
||||
self.optimizer.step()
|
||||
|
||||
total_loss += loss.item() * google_img.size(0)
|
||||
total_samples += google_img.size(0)
|
||||
|
||||
decoded_output = self.model.decode_output(output)
|
||||
mse_components = self.criterion.compute_mse_components(decoded_output, target)
|
||||
mse_trans_sum += mse_components['trans'] * google_img.size(0)
|
||||
mse_angle_sum += mse_components['angle'] * google_img.size(0)
|
||||
mse_scale_sum += mse_components['scale'] * google_img.size(0)
|
||||
|
||||
pbar.set_postfix({"loss": loss.item()})
|
||||
|
||||
self.train_mse_trans.append(mse_trans_sum / total_samples)
|
||||
self.train_mse_angle.append(mse_angle_sum / total_samples)
|
||||
self.train_mse_scale.append(mse_scale_sum / total_samples)
|
||||
|
||||
return {"loss": total_loss / total_samples}
|
||||
|
||||
def validate(self):
|
||||
self.model.eval()
|
||||
total_loss, total_samples = 0, 0
|
||||
mse_trans_sum, mse_angle_sum, mse_scale_sum = 0, 0, 0
|
||||
with torch.no_grad():
|
||||
for batch in tqdm(self.val_loader, desc="Validation"):
|
||||
google_img = batch["google_img"].to(self.device)
|
||||
yandex_img = batch["yandex_img"].to(self.device)
|
||||
target = batch["homography_params"].to(self.device)
|
||||
output = self.model(google_img, yandex_img)
|
||||
decoded_output = self.model.decode_output(output)
|
||||
loss = self.criterion(output, target)
|
||||
total_loss += loss.item() * google_img.size(0)
|
||||
total_samples += google_img.size(0)
|
||||
|
||||
mse_components = self.criterion.compute_mse_components(decoded_output, target)
|
||||
mse_trans_sum += mse_components['trans'] * google_img.size(0)
|
||||
mse_angle_sum += mse_components['angle'] * google_img.size(0)
|
||||
mse_scale_sum += mse_components['scale'] * google_img.size(0)
|
||||
|
||||
self.val_mse_trans.append(mse_trans_sum / total_samples)
|
||||
self.val_mse_angle.append(mse_angle_sum / total_samples)
|
||||
self.val_mse_scale.append(mse_scale_sum / total_samples)
|
||||
|
||||
return {"loss": total_loss / total_samples}
|
||||
|
||||
def train(self, num_epochs):
|
||||
log_dir = config["output_dir"]
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
self.writer = SummaryWriter(log_dir)
|
||||
|
||||
for epoch in range(1, num_epochs + 1):
|
||||
train_metrics = self.train_epoch(epoch)
|
||||
val_metrics = self.validate()
|
||||
self.train_losses.append(train_metrics["loss"])
|
||||
self.val_losses.append(val_metrics["loss"])
|
||||
print(f"Train Loss: {train_metrics['loss']:.4f}, Val Loss: {val_metrics['loss']:.4f}")
|
||||
print(f" MSE - Trans: {self.val_mse_trans[-1]:.4f}, Angle: {self.val_mse_angle[-1]:.4f}, Scale: {self.val_mse_scale[-1]:.4f}")
|
||||
|
||||
if val_metrics["loss"] < self.best_val_loss:
|
||||
self.best_val_loss = val_metrics["loss"]
|
||||
self.save_checkpoint(epoch, is_best=True)
|
||||
print(f"Best model saved (val loss: {val_metrics['loss']:.4f})")
|
||||
|
||||
if epoch % config["save_every_n_epochs"] == 0:
|
||||
self.save_checkpoint(epoch, is_best=False)
|
||||
print(f"Checkpoint saved at epoch {epoch}")
|
||||
|
||||
self.writer.close()
|
||||
|
||||
def save_checkpoint(self, epoch, is_best=False):
|
||||
ckpt_dir = os.path.join(config["output_dir"], "checkpoints")
|
||||
os.makedirs(ckpt_dir, exist_ok=True)
|
||||
ckpt = {"epoch": epoch, "model_state_dict": self.model.state_dict(), "val_loss": self.best_val_loss}
|
||||
torch.save(ckpt, os.path.join(ckpt_dir, f"checkpoint_epoch_{epoch}.pt"))
|
||||
if is_best:
|
||||
torch.save(ckpt, os.path.join(ckpt_dir, "best_model.pt"))
|
||||
59
models/SiaN/src/utils.py
Normal file
59
models/SiaN/src/utils.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
config = {
|
||||
"data_dir": r"C:\Users\admin\Projects\autopilot\datasets\ya_go_maps\images",
|
||||
"image_size": (256, 256),
|
||||
"batch_size": 32,
|
||||
"train_split": 0.8,
|
||||
"num_workers": 0,
|
||||
"epochs": 10,
|
||||
"learning_rate": 2e-4,
|
||||
"dropout_rate": 0.5,
|
||||
"backbone": "resnet18",
|
||||
"output_dir": r"C:\Users\admin\Projects\autopilot\models\SiaN\runs",
|
||||
"save_every_n_epochs": 15,
|
||||
}
|
||||
|
||||
|
||||
def get_camera_matrix(w, h):
|
||||
return np.array([[w / 2, 0, w / 2], [0, h / 2, h / 2], [0, 0, 1]], dtype=np.float32)
|
||||
|
||||
|
||||
def generate_random_homography_params(angle_range=10, translation_range=0.1, scale_range=(0.9, 1.1)):
|
||||
scale = np.random.uniform(*scale_range)
|
||||
tx = np.random.uniform(-translation_range, translation_range)
|
||||
ty = np.random.uniform(-translation_range, translation_range)
|
||||
rx = np.radians(np.random.uniform(-angle_range, angle_range))
|
||||
ry = np.radians(np.random.uniform(-angle_range, angle_range))
|
||||
rz = np.radians(np.random.uniform(-angle_range, angle_range))
|
||||
return np.array([tx, ty, rx, ry, rz, scale])
|
||||
|
||||
|
||||
def homography_params_to_matrix(params, K):
|
||||
tx, ty, rx, ry, rz, scale = params
|
||||
cy, sy = np.cos(rz), np.sin(rz)
|
||||
cp, sp = np.cos(ry), np.sin(ry)
|
||||
cr, sr = np.cos(rx), np.sin(rx)
|
||||
Rz = np.array([[cy, -sy, 0], [sy, cy, 0], [0, 0, 1]], dtype=np.float32)
|
||||
Ry = np.array([[cp, 0, sp], [0, 1, 0], [-sp, 0, cp]], dtype=np.float32)
|
||||
Rx = np.array([[1, 0, 0], [0, cr, -sr], [0, sr, cr]], dtype=np.float32)
|
||||
T = np.array([[1, 0, tx], [0, 1, ty], [0, 0, scale]], dtype=np.float32)
|
||||
return K @ Rx @ Ry @ Rz @ T @ np.linalg.inv(K)
|
||||
|
||||
|
||||
def matrix_to_homography_params(H, K):
|
||||
if hasattr(H, 'numpy'):
|
||||
H = H.numpy()
|
||||
K_inv = np.linalg.inv(K)
|
||||
E = K_inv @ H @ K
|
||||
scale = E[2, 2]
|
||||
R_normalized = E / scale
|
||||
rz = np.arctan2(R_normalized[1, 0], R_normalized[0, 0])
|
||||
ry = np.arctan2(-R_normalized[2, 0], np.sqrt(R_normalized[2, 1]**2 + R_normalized[2, 2]**2))
|
||||
rx = np.arctan2(R_normalized[2, 1], R_normalized[2, 2])
|
||||
A = R_normalized[:2, :2]
|
||||
correction = scale * np.array([R_normalized[0, 2], R_normalized[1, 2]])
|
||||
tx, ty = np.linalg.solve(A, E[:2, 2] - correction)
|
||||
return np.array([tx, ty, rx, ry, rz, scale], dtype=np.float32)
|
||||
1
notebooks/autopilot-gan_3-channels.ipynb
Normal file
1
notebooks/autopilot-gan_3-channels.ipynb
Normal file
File diff suppressed because one or more lines are too long
20
todo.md
20
todo.md
@@ -2,16 +2,16 @@
|
||||
[!] Проверка корректности выявления ориентира на кадре
|
||||
[!] Исправление коррекции координат на основе сопоставления с ориентиром
|
||||
|
||||
[-] FPS счетчик
|
||||
| [-] Оптимизация детекции точек
|
||||
[-] Оформление статистики при тестовых запусках
|
||||
[-] Проведение тестовых запусков
|
||||
[-] Оформление отчета
|
||||
[-] Эксперименты с разными детекторами (SIFT, KAZE)
|
||||
[+] FPS счетчик
|
||||
| [+] Оптимизация детекции точек
|
||||
[+] Оформление статистики при тестовых запусках
|
||||
[+] Проведение тестовых запусков
|
||||
[+] Оформление отчета
|
||||
[+] Эксперименты с разными детекторами (SIFT, KAZE)
|
||||
|
||||
[?] Изменение масштаба во время полёта, обработка этой трансформации
|
||||
[?] Поворот ориентиров
|
||||
[?] Ограничение выбора точек при построении маршрута, чтобы ориентиры полностью попадали в кадр
|
||||
[+] Изменение масштаба во время полёта, обработка этой трансформации
|
||||
[+] Поворот ориентиров
|
||||
[+] Ограничение выбора точек при построении маршрута, чтобы ориентиры полностью попадали в кадр
|
||||
|
||||
[+] График межкадрового смещения
|
||||
| [+] График межкадровых смещениях по версии матрицы гомографии
|
||||
@@ -21,4 +21,4 @@
|
||||
|
||||
[+] Переделать ключевые точки -> Optical Flow
|
||||
[+] Добавить перспективу
|
||||
[ ] Эталоны на Google Maps, полёт тот же
|
||||
[+] Эталоны на Google Maps, полёт тот же
|
||||
|
||||
Reference in New Issue
Block a user