import math import os from io import BytesIO from time import sleep from PIL import Image import cv2 import numpy as np from geolocation import Geolocation from vision_chunk import VisionChunk from yandex_map import YandexMap class Simulator: def __init__(self, yandex_map: YandexMap = None): self.yandex_map = yandex_map self.geo = Geolocation(0, 0, 1, 0) # Параметры камеры (в градусах) self.pitch = 0.0 # тангаж (-10 до 10) self.roll = 0.0 # крен (-10 до 10) os.makedirs('./images', exist_ok=True) def set_pitch(self, pitch: float): """Установить тангаж камеры (градусы, -10 до 10)""" self.pitch = max(-10, min(10, pitch)) def set_roll(self, roll: float): """Установить крен камеры (градусы, -10 до 10)""" self.roll = max(-10, min(10, roll)) def _calculate_camera_angles(self, velocity: float, dangle: float): """Автоматический расчёт тангажа и крена на основе скорости и поворота""" # Тангаж: чем больше скорость, тем больше тангаж (до 10°) self.pitch = min(10, velocity / 10) # Крен: чем больше угол поворота, тем больше крен self.roll = max(-10, min(10, math.degrees(dangle) * 2)) def _get_perspective_points(self, image_width: int, image_height: int, yaw_deg: float) -> tuple: """ Вычисляет 4 точки для перспективной трансформации. Учитывает тангаж, крен, поворот и масштаб. """ center_x = image_width / 2 center_y = image_height / 2 # Базовый размер области захвата (квадрат) base_size = min(image_width, image_height) * 0.7 half_size = base_size / 2 # Исходные углы квадрата (до применения перспективы) corners = np.float32([ [center_x - half_size, center_y - half_size], # top-left [center_x + half_size, center_y - half_size], # top-right [center_x + half_size, center_y + half_size], # bottom-right [center_x - half_size, center_y + half_size], # bottom-left ]) # Применяем смещения для имитации тангажа (pitch) # Положительный тангаж - дрон наклонён вперёд, камера смотрит дальше pitch_offset = self.pitch * 3 # коэффициент для видимого эффекта corners[0][1] -= pitch_offset # top-left y corners[1][1] -= pitch_offset # top-right y corners[2][1] += pitch_offset # bottom-right y corners[3][1] += pitch_offset # bottom-left y # Применяем смещения для имитации крена (roll) # Положительный крен - дрон наклонён вправо roll_offset = self.roll * 3 corners[0][0] += roll_offset # top-left x corners[1][0] -= roll_offset # top-right x corners[2][0] -= roll_offset # bottom-right x corners[3][0] += roll_offset # bottom-left x # Поворот вокруг центра (yaw) angle_rad = math.radians(yaw_deg) cos_a = math.cos(angle_rad) sin_a = math.sin(angle_rad) rotated_corners = [] for corner in corners: # Смещаем к началу координат x = corner[0] - center_x y = corner[1] - center_y # Поворачиваем new_x = x * cos_a - y * sin_a new_y = x * sin_a + y * cos_a # Возвращаем обратно rotated_corners.append([new_x + center_x, new_y + center_y]) return np.float32(rotated_corners) def _apply_perspective_transform(self, image: Image.Image) -> Image.Image: """ Применяет перспективную трансформацию к изображению. Возвращает квадратное изображение 700x700. """ img_array = np.array(image) h, w = img_array.shape[:2] # Получаем исходные точки с учётом перспективы yaw_deg = -math.degrees(self.geo.angle) src_points = self._get_perspective_points(w, h, yaw_deg) # Целевые точки - квадрат 700x700 dst_points = np.float32([ [0, 0], [700, 0], [700, 700], [0, 700] ]) # Вычисляем матрицу трансформации matrix = cv2.getPerspectiveTransform(src_points, dst_points) # Применяем трансформацию transformed = cv2.warpPerspective(img_array, matrix, (700, 700)) return Image.fromarray(transformed) def update_trajectory(self, dx: float, dy: float): """Обновляет координаты дрона""" self.geo.x += dx * self.geo.z self.geo.y += dy * self.geo.z def handle(self, dangle: float, velocity: float = 50) -> None: """ Управление движением дрона. dangle - изменение угла курса (радианы) velocity - скорость движения """ from selenium.webdriver.common.by import By from selenium.webdriver.common.action_chains import ActionChains # Автоматический расчёт углов камеры self._calculate_camera_angles(velocity, dangle) html = self.yandex_map.driver.find_element(By.TAG_NAME, 'html') action = ActionChains(self.yandex_map.driver) action.move_to_element_with_offset(html, 200, 200) action.click_and_hold() self.geo.angle += dangle velocity = max(velocity, 10) dx = math.cos(math.pi / 2 + self.geo.angle) * velocity / self.geo.z dy = math.sin(math.pi / 2 + self.geo.angle) * velocity / self.geo.z self.update_trajectory(dx, dy) action.move_by_offset(-dx, dy) action.release() action.perform() def set_zoom(self, zoom_level: float): """Программное изменение масштаба""" self.geo.z = zoom_level def get_chunk(self) -> VisionChunk: """Получить текущий снимок с камеры дрона""" png = self.yandex_map.driver.get_screenshot_as_png() im = Image.open(BytesIO(png)) # Применяем перспективную трансформацию transformed_im = self._apply_perspective_transform(im) return VisionChunk(transformed_im)