feat: add perspective transform

This commit is contained in:
2026-01-10 17:47:32 +03:00
parent 155bf17847
commit 17594bc8fc
6 changed files with 150 additions and 254 deletions

View File

@@ -11,7 +11,9 @@ from timer import Timer
from vision_chunk import VisionChunk
from visualization import VisualizationManager
from geolocation import Geolocation
from position import Position
from constants import CAMERA_HEIGHT
from utility import estimate_transformation_matrix
random.seed(1)
@@ -41,7 +43,7 @@ class PilotCommand:
class AutoPilot(Pilot):
# Состояние одометрии
geo: Geolocation
pos: Position
chunks: list[VisionChunk] # Ориентиры
target_idx: int # Текущий ориентир
@@ -51,17 +53,17 @@ class AutoPilot(Pilot):
prev_chunk: VisionChunk | None
frame_count: int
vis_manager: VisualizationManager # Менеджер визуализации (опционально)
# Положение на основе ориентира
reserved_geo: Geolocation | None
reserved_pos: Position | None
def __init__(self, points = [], chunks = [], viz_manager=None):
self.prev_chunk = None
self.geo = Geolocation(0, 0, 1, 0)
self.pos = Position(0, 0, 1, 0, 0, 0)
self.chunks = chunks
self.frame_count = 0
self.vis_manager = viz_manager # Менеджер визуализации
self.reserved_geo = None
self.reserved_pos = None
# Пороговые значения качества сопоставления/гомографии
self.min_inliers: int = 12
@@ -76,7 +78,7 @@ class AutoPilot(Pilot):
self.timer = Timer()
def get_position(self) -> tuple[float, float]:
return self.geo.x, self.geo.y
return self.pos.x, self.pos.y
def calculate_optical_flow(self, prev_chunk: VisionChunk, current_chunk: VisionChunk):
"""
@@ -87,7 +89,6 @@ class AutoPilot(Pilot):
current_gray = current_chunk.to_cv2_gray()
h, w = prev_gray.shape[:2]
cx, cy = w // 2, h // 2
# Создаем сетку точек для отслеживания (аналогично вашему step=20)
step = 35
@@ -120,99 +121,34 @@ class AutoPilot(Pilot):
return None, None
# Преобразуем в отцентрированные координаты (Y вверх)
src_pts = np.float32([[x - cx, cy - y] for x, y in good_old]).reshape(-1, 1, 2)
dst_pts = np.float32([[x - cx, cy - y] for x, y in good_new]).reshape(-1, 1, 2)
src_pts = np.float32([[x, y] for x, y in good_old]).reshape(-1, 1, 2)
dst_pts = np.float32([[x, y] for x, y in good_new]).reshape(-1, 1, 2)
return src_pts, dst_pts
def estimate_transformation_matrix(self, src_pts: np.ndarray, dst_pts: np.ndarray):
"""
Оценивает матрицу трансформации на основе сопоставленных точек
Точки уже отцентрированы относительно центра изображения
"""
# Используем RANSAC для оценки матрицы гомографии
print("Count of points: ", len(src_pts), len(dst_pts))
H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 3.0, maxIters=1000)
# RH, rmask = cv2.findHomography(dst_pts, src_pts, cv2.RANSAC, 1.0)
if H is None:
return None
# Маска инлайеров
inliers = int(mask.sum()) if mask is not None else 0
total = int(mask.size) if mask is not None else 0
inlier_ratio = (inliers / total) if total > 0 else 0.0
# Ошибка репроекции по инлайерам (RMSE)
rmse = None
try:
# Проецируем src через H и сравниваем с dst
proj = cv2.perspectiveTransform(src_pts, H)
errs = np.linalg.norm((proj - dst_pts).reshape(-1, 2), axis=1)
if mask is not None and mask.shape[0] == errs.shape[0]:
errs = errs[mask.ravel().astype(bool)]
if errs.size > 0:
rmse = float(np.sqrt(np.mean(errs ** 2)))
except Exception:
rmse = None
# Извлекаем параметры трансформации из матрицы гомографии
# H = [a11 a12 tx]
# [a21 a22 ty]
# [0 0 1 ]
# Масштаб и поворот
a11, a12 = H[0, 0], H[0, 1]
a21, a22 = H[1, 0], H[1, 1]
# Смещение (уже отцентрировано)
tx, ty = -H[0, 2], -H[1, 2]
# print(" [Pilot] translate:", tx, ty)
# Вычисляем угол поворота
angle = -np.arctan2(a21, a11)
# Вычисляем масштаб
scale_x = np.sqrt(a11**2 + a21**2)
scale_y = np.sqrt(a12**2 + a22**2)
scale = (scale_x + scale_y) / 2
return {
'translation': (tx, ty), # Уже отцентрировано
'rotation': angle,
'scale': scale,
'homography': H,
'mask': mask,
'inliers': inliers,
'inliers_ratio': inlier_ratio,
'rmse': rmse
}
def update_drone_position(self, transformation_info: dict):
def update_drone_position(self, homography_matrix: np.ndarray):
"""
Обновляет позицию и угол БПЛА на основе трансформации изображения
Координаты уже отцентрированы относительно центра изображения
"""
homography = transformation_info['homography']
self.geo @= homography
self.pos.iapply(homography_matrix)
if self.reserved_geo is not None:
self.reserved_geo @= homography
if self.reserved_pos is not None:
self.reserved_pos.iapply(homography_matrix)
def get_drone_state(self) -> dict:
"""
Возвращает текущее состояние БПЛА
"""
return {
'x': self.geo.x,
'y': self.geo.y,
'angle': self.geo.yaw,
'angle_degrees': math.degrees(self.geo.yaw),
'x': self.pos.x,
'y': self.pos.y,
'angle': self.pos.yaw,
'angle_degrees': math.degrees(self.pos.yaw),
'frame_count': self.frame_count
}
def get_position_by_chunk(self) -> Geolocation | None:
def get_position_by_chunk(self) -> Position | None:
# Пытаемся найти ориентир на картинке:
current_chunk = self.prev_chunk
landmark_chunk = self.chunks[self.target_idx]
@@ -244,7 +180,7 @@ class AutoPilot(Pilot):
# if False:
# Считаем абсолютную позу относительно координат ориентира
landmark_world_x, landmark_world_y = self.points[self.target_idx]
landmark = Geolocation(landmark_world_x, landmark_world_y, 1, 0)
landmark = Position(landmark_world_x, landmark_world_y, 1, 0)
homography = landmark_transform['homography']
# homography = np.linalg.inv(homography)
print(f" [Pilot] Landmark correction applied (inliers={landmark_transform['inliers']}, ratio={ratio:.2f}, rmse={rmse:.2f})")
@@ -260,55 +196,53 @@ class AutoPilot(Pilot):
self.prev_chunk = current_chunk
self.timer.stop()
return PilotCommand(processing_time=self.timer.get_elapsed())
# Расстояние до цели
distance_to_target = math.sqrt(
(self.points[self.target_idx][0] - self.geo.x) ** 2 +
(self.points[self.target_idx][1] - self.geo.y) ** 2
(self.points[self.target_idx][0] - self.pos.x) ** 2 +
(self.points[self.target_idx][1] - self.pos.y) ** 2
)
# Вычисляем оптический поток для покадрового сравнения
optical_flow_timer = Timer()
optical_flow_timer.start()
src_pts, dst_pts = self.calculate_optical_flow(self.prev_chunk, current_chunk)
optical_flow_timer.stop()
print(f"Optical flow calculating: {optical_flow_timer.get_elapsed() * 1000:.2f} ms")
matching_timer = Timer()
matching_timer.start()
# src_pts, dst_pts = self.calculate_optical_flow(self.prev_chunk, current_chunk)
src_pts, dst_pts, _, _, _ = self.prev_chunk.detect_and_match_keypoints(current_chunk)
matching_timer.stop()
print(f"Matching calculating: {matching_timer.get_elapsed() * 1000:.2f} ms")
# Оцениваем смещение БПЛА
if src_pts is not None and dst_pts is not None:
# Оцениваем матрицу трансформации
matrix_estimation_timer = Timer()
matrix_estimation_timer.start()
transformation_info = self.estimate_transformation_matrix(src_pts, dst_pts)
homography_matrix = estimate_transformation_matrix(src_pts, dst_pts)
matrix_estimation_timer.stop()
print(f"Transformation matrix estimating: {matrix_estimation_timer.get_elapsed() * 1000:.2f} ms")
if transformation_info:
# Обновляем позицию и угол БПЛА (одометрия по кадрам)
self.update_drone_position(transformation_info)
print(f"Transformation matrix updating: {matrix_estimation_timer.get_elapsed() * 1000:.2f} ms")
if homography_matrix is not None:
self.update_drone_position(homography_matrix)
self.timer.stop()
# Обновляем визуализацию
if self.vis_manager:
# Используем визуализацию движения точек по сетке
homography_matrix = transformation_info['homography']
self.vis_manager.update_homography_grid(
current_chunk.to_numpy(),
homography_matrix,
grid_step=85)
current_chunk.to_numpy(),
homography_matrix,
grid_step=35)
self.timer.start()
chunk_timer = Timer()
chunk_timer.start()
# Пытаемся найти ориентир на картинке:
self.prev_chunk = current_chunk
# npos = self.get_position_by_chunk()
# if npos is not None:
# self.reserved_geo = npos
# self.reserved_pos = npos
chunk_timer.stop()
print(f"Chunk timer: {chunk_timer.get_elapsed() * 1000:.2f} ms")
@@ -320,18 +254,18 @@ class AutoPilot(Pilot):
return PilotCommand(stop=True)
distance_to_target = math.sqrt(
(self.points[self.target_idx][0] - self.geo.x) ** 2 +
(self.points[self.target_idx][1] - self.geo.y) ** 2
(self.points[self.target_idx][0] - self.pos.x) ** 2 +
(self.points[self.target_idx][1] - self.pos.y) ** 2
)
if self.reserved_geo is not None:
self.geo = self.reserved_geo
self.reserved_geo = None
if self.reserved_pos is not None:
self.pos = self.reserved_pos
self.reserved_pos = None
# Вычисляем угол к цели
target_angle = math.atan2(self.points[self.target_idx][1] - self.geo.y, self.points[self.target_idx][0] - self.geo.x)
target_angle = math.atan2(self.points[self.target_idx][1] - self.pos.y, self.points[self.target_idx][0] - self.pos.x)
angle_trajectory = self.geo.yaw + math.pi / 2
angle_trajectory = self.pos.yaw + math.pi / 2
# print("[ANGLE]", angle_trajectory, "->", target_angle)
@@ -355,4 +289,4 @@ class AutoPilot(Pilot):
def reset_position(self, x: float = 0.0, y: float = 0.0, angle: float = 0.0):
"""Сбрасывает позицию и угол БПЛА"""
self.geo = Geolocation(x, y, 1, angle)
self.pos = Position(x, y, 1, angle)