diff --git a/README.md b/README.md index 4a509d6..c185c97 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,13 @@ python main.py --mode standalone --name test_route Можно выполнить шаги отдельно: ```powershell +python main.py --mode markup --name test_route python main.py --mode build --name test_route python main.py --mode run --name test_route ``` +`markup` только скачивает карту и сохраняет разметку маршрута. `build` наполняет маршрут chunks и служебными данными. Если при `build` папки маршрута ещё нет, сначала автоматически выполнится разметка, затем наполнение. `standalone` выполняет весь путь: разметка, build и run. + Дополнительно можно указать координаты и карты: ```powershell @@ -39,7 +42,7 @@ python main.py --mode standalone --name test_route --lat 49.103814 --lon 55.7942 ## Флаги -- `--mode` - режим работы: `standalone`, `build`, `run`. +- `--mode` - режим работы: `standalone`, `markup`, `build`, `run`. - `--name` - название маршрута. - `--lat`, `--lon` - координаты начальной области. - `--reference` - карта для эталонных изображений: `google` или `yandex`. @@ -49,19 +52,27 @@ python main.py --mode standalone --name test_route --lat 49.103814 --lon 55.7942 - `--debug-landmark` - вывести отладку ориентиров. - `--use-sian-similarity` - выбирать ориентир через SiaN Similarity. - `--use-gan` - преобразовывать эталонные изображения через GAN. +- `--no-landmarks` - отключить эталоны и лететь только по межкадровой одометрии. - `--interframe-method` - метод межкадрового сравнения: `optical-flow`, `orb`, `akaze`, `sift`, `brisk`. - `--landmark-method` - метод сравнения с эталонами: `orb`, `akaze`, `sift`, `brisk`. ## Автоматические серии запусков -Для прогона уже построенных маршрутов с разными параметрами: +`run_batch.ps1` запускает серию экспериментов по уже построенным маршрутам. Скрипт не делает ручную разметку: маршруты должны быть заранее подготовлены через `markup` и `build`, либо через `standalone`. Каждый запуск вызывает `python main.py --mode run ...`, а результаты сохраняются обычным способом в `test_runs`. + +Минимальный запуск для одного маршрута: ```powershell -.\run_batch.ps1 -Routes test_route -SimulationMaps yandex,google -InterframeMethods optical-flow,orb,akaze -LandmarkMethods orb,sift -RefMinDistances 75,100 +.\run_batch.ps1 -Routes test_route ``` -Если `-Routes` не указан, скрипт запустит все маршруты из `trajectories`. -Маршруты можно указать списком: +Если `-Routes` не указан, скрипт запустит все маршруты из `trajectories`, у которых есть `positions.pkl`: + +```powershell +.\run_batch.ps1 +``` + +Несколько маршрутов можно указать через запятую: ```powershell .\run_batch.ps1 -Routes 2026-05-31_15-32-53,2026-05-31_15-21-17 @@ -73,8 +84,73 @@ python main.py --mode standalone --name test_route --lat 49.103814 --lon 55.7942 .\run_batch.ps1 -RouteListPath .\routes.txt ``` +Пример `routes.txt`: + +```text +2026-05-31_15-32-53 +2026-05-31_15-21-17 +# строки с комментариями игнорируются +``` + +Прогон разных методов межкадрового сравнения: + +```powershell +.\run_batch.ps1 ` + -Routes test_route ` + -InterframeMethods optical-flow,orb,akaze,sift,brisk ` + -LandmarkMethods orb +``` + +Прогон разных методов эталонов: + +```powershell +.\run_batch.ps1 ` + -Routes test_route ` + -InterframeMethods optical-flow ` + -LandmarkMethods orb,akaze,sift,brisk +``` + +Прогон с эталонами и без эталонов: + +```powershell +.\run_batch.ps1 ` + -Routes test_route ` + -UseLandmarksValues true,false +``` + +Полная матрица экспериментов: + +```powershell +.\run_batch.ps1 ` + -Routes test_route ` + -SimulationMaps yandex,google ` + -InterframeMethods optical-flow,orb,akaze ` + -LandmarkMethods orb,sift ` + -UseLandmarksValues true,false ` + -UseSianSimilarityValues false,true ` + -UseGanValues false,true ` + -RefMinDistances 75,100 +``` + Для проверки команд без запуска Selenium: ```powershell .\run_batch.ps1 -DryRun ``` + +### Флаги `run_batch.ps1` + +| Флаг | Описание | Пример | +|---|---|---| +| `-Routes` | Список имён маршрутов из `trajectories`. Можно через запятую. | `-Routes route1,route2` | +| `-RouteListPath` | Путь к txt-файлу со списком маршрутов, по одному на строку. | `-RouteListPath .\routes.txt` | +| `-SimulationMaps` | Карты, на которых выполнять симуляцию. | `-SimulationMaps yandex,google` | +| `-InterframeMethods` | Методы межкадрового сравнения. | `-InterframeMethods optical-flow,orb,sift` | +| `-LandmarkMethods` | Методы сравнения с эталонами. | `-LandmarkMethods orb,akaze,brisk` | +| `-UseLandmarksValues` | Использовать эталоны или лететь только по межкадровой одометрии. | `-UseLandmarksValues true,false` | +| `-UseSianSimilarityValues` | Включать выбор эталона через SiaN Similarity. | `-UseSianSimilarityValues false,true` | +| `-UseGanValues` | Включать GAN-преобразование эталонных изображений. | `-UseGanValues false,true` | +| `-RefMinDistances` | Минимальное расстояние между загружаемыми эталонами. | `-RefMinDistances 50,100,150` | +| `-UseSianSimilarity` | Быстрый вариант `-UseSianSimilarityValues true`. | `-UseSianSimilarity` | +| `-UseGan` | Быстрый вариант `-UseGanValues true`. | `-UseGan` | +| `-DryRun` | Только напечатать команды, не запускать Selenium и симуляцию. | `-DryRun` | diff --git a/autopilot.py b/autopilot.py index 277df52..7a08e00 100644 --- a/autopilot.py +++ b/autopilot.py @@ -76,6 +76,7 @@ class AutoPilot(Pilot): use_gan: bool = False, interframe_method: str = "optical_flow", landmark_method: str = "orb", + use_landmarks: bool = True, ): self.prev_chunk = None self.pos = Position(0, 0, 1, 0, 0, 0) @@ -86,6 +87,7 @@ class AutoPilot(Pilot): self.pixel_ratio = pixel_ratio self.use_sian_similarity = use_sian_similarity self.use_gan = use_gan + self.use_landmarks = use_landmarks if interframe_method not in INTERFRAME_METHODS: raise ValueError(f"Unsupported interframe method: {interframe_method}") if landmark_method not in FEATURE_METHODS: @@ -191,7 +193,7 @@ class AutoPilot(Pilot): landmark_timer.start() current_chunk = VisionChunk(self.prev_chunk.image, self.landmark_method) if self.prev_chunk is not None else None - if current_chunk is None or not self.chunks: + if not self.use_landmarks or current_chunk is None or not self.chunks: return None if self.use_sian_similarity: @@ -367,7 +369,7 @@ class AutoPilot(Pilot): # Пытаемся найти ориентир на картинке: self.prev_chunk = current_chunk # Для улучшения среднего FPS - if self.frame_count % 5 == 0: + if self.use_landmarks and self.frame_count % 5 == 0: pos_by_chunk = self.get_position_by_chunk() if pos_by_chunk is not None: self.pos = pos_by_chunk diff --git a/main.py b/main.py index 6fe8d7e..74beab0 100644 --- a/main.py +++ b/main.py @@ -75,35 +75,87 @@ def move_map_safely(online_map: YandexMap | GoogleMap, dx: float, dy: float, ste def normalize_interframe_method(method: str) -> str: return "optical_flow" if method == "optical-flow" else method -def build(name: str, map_name: str, lat: float, lon: float): - - # Создание папки с информацией о маршруте +def get_trajectory_dir(name: str) -> Path: dir = Path('trajectories') if not dir.exists(): dir.mkdir() - dir /= name - assert not dir.exists() + return dir / name + +def markup(name: str, map_name: str, lat: float, lon: float): + dir = get_trajectory_dir(name) + assert not dir.exists(), "Маршрут уже существует" dir.mkdir() - dir_chunks = dir / 'chunks' - dir_chunks.mkdir() - make_global_photo('map.jpg', map_name, lat, lon, 15) - map_path = dir / 'map.jpg' - shutil.copyfile('map.jpg', map_path) - - points = get_trajectory_points(str(map_path)) map_zoom = 15 flight_zoom = 18 map_pixel_ratio = get_pixel_ratio(map_name, map_zoom) + map_path = dir / 'map.jpg' online_map: YandexMap | GoogleMap = get_map(map_name, lat, lon, map_zoom) - - + online_map.save_photo(str(map_path)) width, height = online_map.get_size() + online_map.destroy() + + points = get_trajectory_points(str(map_path)) points_coords = np.array(list(map(lambda p: [ (p[0] - points[0][0]) * width, (points[0][1] - p[1]) * height ], points))) points_coords_pixels = points_coords.copy() - points_coords *= online_map.pixel_ratio + points_coords *= map_pixel_ratio + + data = { + 'points': points_coords, + 'drawn_points': points, + 'map_image': 'map.jpg', + 'map_size': (width, height), + 'map_zero_point': points[0], + 'map_provider': map_name, + 'map_zoom': map_zoom, + 'flight_zoom': flight_zoom, + 'map_pixel_ratio': map_pixel_ratio, + 'build_params': { + 'name': name, + 'reference': map_name, + 'lat': lat, + 'lon': lon, + 'map_zoom': map_zoom, + 'flight_zoom': flight_zoom, + }, + 'map_extent': make_map_extent(width, height, points[0], map_pixel_ratio), + 'route_length': calc_polyline_length(points_coords), + 'route_length_meters': calc_polyline_length(points_coords), + 'route_length_pixels': calc_polyline_length(points_coords_pixels), + } + + with (dir / 'positions.pkl').open('wb') as file: + pickle.dump(data, file) + + print(points_coords) + print("WRITE POINTS:", points) + +def build(name: str, map_name: str, lat: float, lon: float): + dir = get_trajectory_dir(name) + if not dir.exists(): + markup(name, map_name, lat, lon) + + dir_chunks = dir / 'chunks' + dir_chunks.mkdir(exist_ok=True) + + file_positions = dir / 'positions.pkl' + with file_positions.open('rb') as file: + data = pickle.load(file) + + build_params = data.get('build_params', {}) + map_name = data.get('map_provider', build_params.get('reference', map_name)) + lat = build_params.get('lat', lat) + lon = build_params.get('lon', lon) + points_coords = data['points'] + points = data.get('drawn_points') + if points is None: + points = [data['map_zero_point']] + + map_zoom = data.get('map_zoom', 15) + flight_zoom = data.get('flight_zoom', 18) + online_map: YandexMap | GoogleMap = get_map(map_name, lat, lon, map_zoom) # Начнём симуляцию полёта с первой точки online_map.make_as_center(*points[0]) @@ -158,17 +210,13 @@ def build(name: str, map_name: str, lat: float, lon: float): simulator.pos.y = 0 sleep(1.5) - data = { + data.update({ 'points': points_coords, 'chunk_positions': positions, 'initial_geolocation': geo, - 'map_image': 'map.jpg', - 'map_size': (width, height), - 'map_zero_point': points[0], 'map_provider': map_name, 'map_zoom': map_zoom, 'flight_zoom': flight_zoom, - 'map_pixel_ratio': map_pixel_ratio, 'flight_pixel_ratio': online_map.pixel_ratio, 'build_params': { 'name': name, @@ -178,18 +226,13 @@ def build(name: str, map_name: str, lat: float, lon: float): 'map_zoom': map_zoom, 'flight_zoom': flight_zoom, }, - 'map_extent': make_map_extent(width, height, points[0], map_pixel_ratio), - 'route_length': calc_polyline_length(points_coords), - 'route_length_meters': calc_polyline_length(points_coords), - 'route_length_pixels': calc_polyline_length(points_coords_pixels), 'build_flight_length': calc_polyline_length([[pos.x, pos.y] for pos in positions]), 'build_flight_length_meters': calc_polyline_length([[pos.x, pos.y] for pos in positions]), 'build_flight_length_pixels': calc_polyline_length([[pos.x / online_map.pixel_ratio, pos.y / online_map.pixel_ratio] for pos in positions]), - } + }) print(points_coords) - file_positions = dir / 'positions.pkl' with file_positions.open('wb') as file: pickle.dump(data, file) @@ -206,6 +249,7 @@ def run( use_gan: bool = False, interframe_method: str = "optical_flow", landmark_method: str = "orb", + use_landmarks: bool = True, run_params: dict | None = None, ): dir = Path('trajectories') @@ -218,6 +262,8 @@ def run( with file_positions.open('rb') as file: data = pickle.load(file) + + assert 'initial_geolocation' in data, "Маршрут размечен, но не наполнен chunks. Сначала запустите --mode build." initial_geolocation = data['initial_geolocation'] @@ -227,15 +273,16 @@ def run( sleep(2) chunks: list[VisionChunk] = [] - for i in range(len(data['chunk_positions'])): - pos = data['chunk_positions'][i] - chunk_path = dir_chunks / f"chunk_{i}.png" - if not chunk_path.exists(): - continue - if len(chunks) == 0 or np.hypot(chunks[-1].pos.x - pos.x, chunks[-1].pos.y - pos.y) > ref_min_distance: - chunk = VisionChunk.load_image(chunk_path, landmark_method) - chunk.pos = data['chunk_positions'][i] / online_map.pixel_ratio - chunks.append(chunk) + if use_landmarks: + for i in range(len(data['chunk_positions'])): + pos = data['chunk_positions'][i] + chunk_path = dir_chunks / f"chunk_{i}.png" + if not chunk_path.exists(): + continue + if len(chunks) == 0 or np.hypot(chunks[-1].pos.x - pos.x, chunks[-1].pos.y - pos.y) > ref_min_distance: + chunk = VisionChunk.load_image(chunk_path, landmark_method) + chunk.pos = data['chunk_positions'][i] / online_map.pixel_ratio + chunks.append(chunk) r = 0 for i in range(len(data['points']) - 1): @@ -283,6 +330,7 @@ def run( use_gan=use_gan, interframe_method=interframe_method, landmark_method=landmark_method, + use_landmarks=use_landmarks, ) simulator = Simulator(online_map) pilot.target_idx = 0 @@ -391,6 +439,7 @@ def run( 'ref_min_distance': ref_min_distance, 'use_sian_similarity': use_sian_similarity, 'use_gan': use_gan, + 'use_landmarks': use_landmarks, 'interframe_method': interframe_method, 'landmark_method': landmark_method, }, @@ -435,8 +484,8 @@ def parse_args(): '--mode', type=str, required=True, - choices=['standalone', 'build', 'run'], - help='Режим работы: standalone, build или run' + choices=['standalone', 'markup', 'build', 'run'], + help='Режим работы: standalone, markup, build или run' ) # Добавляем опциональный аргумент --name @@ -514,6 +563,21 @@ def parse_args(): help='Преобразовывать эталонный vision_chunk через GAN перед поиском ключевых точек' ) + parser.add_argument( + '--use-landmarks', + dest='use_landmarks', + action='store_true', + default=True, + help='Использовать эталоны для коррекции позиции (по умолчанию включено)' + ) + + parser.add_argument( + '--no-landmarks', + dest='use_landmarks', + action='store_false', + help='Не использовать эталоны, лететь только по межкадровой одометрии' + ) + parser.add_argument( '--interframe-method', type=str, @@ -535,8 +599,8 @@ def parse_args(): # Парсим аргументы args = parser.parse_args() - # Проверяем, что для build и run указан --name - if args.mode in ['build', 'run'] and not args.name: + # Проверяем, что для markup, build и run указан --name + if args.mode in ['markup', 'build', 'run'] and not args.name: parser.error(f"--name обязателен для режима {args.mode}") return args @@ -557,6 +621,9 @@ if __name__ == "__main__": constants.DEBUG_FPS = args.debug_fps constants.DEBUG_LANDMARK = args.debug_landmark + if mode == 'markup': + markup(name, ref, lat, lon) + if mode == 'build' or mode == 'standalone': build(name, ref, lat, lon) @@ -569,6 +636,7 @@ if __name__ == "__main__": args.use_gan, interframe_method, landmark_method, + args.use_landmarks, { 'mode': mode, 'name': name, @@ -581,6 +649,7 @@ if __name__ == "__main__": 'debug_landmark': args.debug_landmark, 'use_sian_similarity': args.use_sian_similarity, 'use_gan': args.use_gan, + 'use_landmarks': args.use_landmarks, 'interframe_method': interframe_method, 'landmark_method': landmark_method, }, diff --git a/map.jpg b/map.jpg index 56ac1b5..ae7cf1c 100644 Binary files a/map.jpg and b/map.jpg differ diff --git a/run_batch.ps1 b/run_batch.ps1 index 341a7a5..18e34e6 100644 --- a/run_batch.ps1 +++ b/run_batch.ps1 @@ -4,6 +4,9 @@ param( [string[]]$SimulationMaps = @("yandex"), [string[]]$InterframeMethods = @("optical-flow", "orb", "akaze", "sift", "brisk"), [string[]]$LandmarkMethods = @("orb", "akaze", "sift", "brisk"), + [string[]]$UseSianSimilarityValues = @("false"), + [string[]]$UseGanValues = @("false"), + [string[]]$UseLandmarksValues = @("true"), [double[]]$RefMinDistances = @(100), [switch]$UseSianSimilarity, [switch]$UseGan, @@ -29,6 +32,29 @@ $Routes = Expand-List $Routes $SimulationMaps = Expand-List $SimulationMaps $InterframeMethods = Expand-List $InterframeMethods $LandmarkMethods = Expand-List $LandmarkMethods +$UseSianSimilarityValues = Expand-List $UseSianSimilarityValues +$UseGanValues = Expand-List $UseGanValues +$UseLandmarksValues = Expand-List $UseLandmarksValues + +if ($UseSianSimilarity) { + $UseSianSimilarityValues = @("true") +} +if ($UseGan) { + $UseGanValues = @("true") +} + +function Convert-ToBool { + param([string]$Value) + + $Normalized = $Value.Trim().ToLowerInvariant() + if ($Normalized -in @("true", "1", "yes", "y", "on")) { + return $true + } + if ($Normalized -in @("false", "0", "no", "n", "off")) { + return $false + } + throw "Не удалось распознать boolean значение: $Value" +} if ($RouteListPath) { if (-not (Test-Path $RouteListPath)) { @@ -59,7 +85,7 @@ if (-not (Test-Path $Python)) { $Python = "python" } -$Total = $Routes.Count * $SimulationMaps.Count * $InterframeMethods.Count * $LandmarkMethods.Count * $RefMinDistances.Count +$Total = $Routes.Count * $SimulationMaps.Count * $InterframeMethods.Count * $LandmarkMethods.Count * $UseSianSimilarityValues.Count * $UseGanValues.Count * $UseLandmarksValues.Count * $RefMinDistances.Count $RunIndex = 0 Write-Host "Batch run started" @@ -71,32 +97,45 @@ foreach ($Route in $Routes) { foreach ($RefMinDistance in $RefMinDistances) { foreach ($InterframeMethod in $InterframeMethods) { foreach ($LandmarkMethod in $LandmarkMethods) { - $RunIndex += 1 - $Args = @( - "main.py", - "--mode", "run", - "--name", $Route, - "--simulation", $SimulationMap, - "--ref-min-distance", "$RefMinDistance", - "--interframe-method", $InterframeMethod, - "--landmark-method", $LandmarkMethod - ) + foreach ($UseSianSimilarityValue in $UseSianSimilarityValues) { + foreach ($UseGanValue in $UseGanValues) { + foreach ($UseLandmarksValue in $UseLandmarksValues) { + $SianEnabled = Convert-ToBool $UseSianSimilarityValue + $GanEnabled = Convert-ToBool $UseGanValue + $LandmarksEnabled = Convert-ToBool $UseLandmarksValue - if ($UseSianSimilarity) { - $Args += "--use-sian-similarity" - } - if ($UseGan) { - $Args += "--use-gan" - } + $RunIndex += 1 + $Args = @( + "main.py", + "--mode", "run", + "--name", $Route, + "--simulation", $SimulationMap, + "--ref-min-distance", "$RefMinDistance", + "--interframe-method", $InterframeMethod, + "--landmark-method", $LandmarkMethod + ) - Write-Host "" - Write-Host "[$RunIndex/$Total] route=$Route simulation=$SimulationMap ref=$RefMinDistance interframe=$InterframeMethod landmark=$LandmarkMethod" - Write-Host "$Python $($Args -join ' ')" + if ($SianEnabled) { + $Args += "--use-sian-similarity" + } + if ($GanEnabled) { + $Args += "--use-gan" + } + if (-not $LandmarksEnabled) { + $Args += "--no-landmarks" + } - if (-not $DryRun) { - & $Python @Args - if ($LASTEXITCODE -ne 0) { - throw "Run failed with exit code $LASTEXITCODE" + Write-Host "" + Write-Host "[$RunIndex/$Total] route=$Route simulation=$SimulationMap ref=$RefMinDistance interframe=$InterframeMethod landmark=$LandmarkMethod landmarks=$LandmarksEnabled sian=$SianEnabled gan=$GanEnabled" + Write-Host "$Python $($Args -join ' ')" + + if (-not $DryRun) { + & $Python @Args + if ($LASTEXITCODE -ne 0) { + throw "Run failed with exit code $LASTEXITCODE" + } + } + } } } }