Лабораторная работа 8. Основы фотограмметрии
Лабораторная работа: Построение 3D-модели по фотографиям (Structure-from-Motion)
Цель: Реализовать инкрементальный алгоритм SfM (Structure-from-Motion) на Python.
Формат: Google Colab / Jupyter Notebook.
Входные данные: Серия из 15–40 фотографий объекта.
Результат: 3D-меш объекта в формате .obj или .ply.
1. Теоретическое введение
Алгоритм Structure-from-Motion (SfM) позволяет восстановить трехмерную структуру сцены по набору двумерных изображений. Процесс состоит из двух этапов:
Инициализация: Восстановление 3D-точек из первой пары изображений (стерео-пара).
Инкрементальное расширение: Добавление новых камер по одной, используя уже найденные 3D-точки для определения положения новой камеры.
В этой работе вам предоставлен код для этапа 1. Ваша задача — реализовать этап 2 (цикл добавления камер) и финальное построение поверхности.
2. Подготовка данных
Сделайте 5–10 фотографий объекта (стул, коробка, статуя) со всех сторон.
Важно: Объект должен быть текстурным (не белая стена, не стекло).
Важно: Между соседними кадрами должно быть перекрытие ~60-70%.
Загрузите фото в папку
data/в вашем проекте.
3. Часть 1: Инициализация (Демонстрация)
Вставьте этот код в ноутбук. Он реализует реконструкцию для первых двух изображений. Разберитесь, как он работает.
Установка библиотек:
!pip install opencv-python opencv-contrib-python open3d plotly
Базовый код (2 изображения):
import cv2
import numpy as np
import open3d as o3d
# --- 1. Настройки ---
# Аппроксимация матрицы камеры K (если нет калибровки)
def get_K(img):
h, w = img.shape[:2]
f = 1.2 * max(w, h) # Эвристика для смартфонов
K = np.array([[f, 0, w/2], [0, f, h/2], [0, 0, 1]])
return K
# --- 2. Загрузка первых двух фото ---
img1 = cv2.imread('data/img1.jpg')
img2 = cv2.imread('data/img2.jpg')
K = get_K(img1)
# --- 3. Поиск соответствий (SIFT) ---
sift = cv2.SIFT_create()
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
# Сопоставление
bf = cv2.BFMatcher()
matches = bf.knnMatch(des1, des2, k=2)
# Фильтрация Lowe's ratio test
good = []
pts1 = []
pts2 = []
for m, n in matches:
if m.distance < 0.75 * n.distance:
good.append(m)
pts1.append(kp1[m.queryIdx].pt)
pts2.append(kp2[m.trainIdx].pt)
pts1 = np.float32(pts1)
pts2 = np.float32(pts2)
# --- 4. Геометрия двух видов ---
# Находим Essential Matrix и позу второй камеры
E, mask = cv2.findEssentialMat(pts1, pts2, K, method=cv2.RANSAC, prob=0.999, threshold=1.0)
pts1 = pts1[mask.ravel() == 1]
pts2 = pts2[mask.ravel() == 1]
# recoverPose возвращает R и t для второй камеры (первая в 0,0,0)
_, R, t, mask = cv2.recoverPose(E, pts1, pts2, K)
# --- 5. Триангуляция ---
# Матрицы проекции: P = K *
P1 = K @ np.hstack((np.eye(3), np.zeros((3, 1))))
P2 = K @ np.hstack((R, t))
# Триангулируем точки
points4D = cv2.triangulatePoints(P1, P2, pts1.T, pts2.T)
points3D = (points4D[:3] / points4D[1]).T # Нормализация
print(f"Инициализация завершена. Восстановлено {len(points3D)} точек.")
4. Часть 2: Инкрементальное добавление (Основное задание)
На данный момент у вас есть облако точек от двух камер. Однако для модели стула двух ракурсов недостаточно. Вам нужно написать цикл, который обработает оставшиеся изображения (3, 4, 5...).
Алгоритм действий (реализуйте это в коде):
Загрузите следующее изображение (
img3).Найдите ключевые точки на
img3и сопоставьте их с точкамиimg2.Ключевой момент (PnP):
У вас есть связи: Точки 3D (из шага 5) <-> Точки 2D на img2 <-> Точки 2D на img3.
Вам нужно найти набор пар: {Существующая 3D точка} — {Ее проекция на img3}.
Используйте функцию
cv2.solvePnPRansacдля найденных пар.Вход: список 3D точек и список их 2D проекций на новом кадре.
Выход: Поза новой камеры ($R_{new}, t_{new}$).
Триангуляция новых точек:
Используя новую позу P_new и позу предыдущей камеры P_old, триангулируйте те точки, которые появились на новом кадре, но еще не были в 3D облаке.
Добавьте новые точки в общий массив
points3D.Повторите для всех оставшихся фото.
Подсказка по PnP:
5. Часть 3: Построение поверхности (Meshing)
В результате Части 2 вы получите разреженное облако точек. Чтобы превратить его в модель для Блендера, нужно "натянуть" на него сетку.
Используйте библиотеку Open3D и метод Alpha Shapes. Это простой способ создать геометрию из точек.
import open3d as o3d
# 1. Создаем объект облака точек Open3D
pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector(points3D) # Ваше итоговое облако
# 2. Очистка от шума (опционально)
cl, ind = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0)
pcd = pcd.select_by_index(ind)
# 3. Создание меша (Alpha shapes)
# alpha - параметр "плотности" обтягивания. Поэкспериментируйте (напр. 0.1, 0.5, 1.0)
alpha = 0.1
mesh = o3d.geometry.TriangleMesh.create_from_point_cloud_alpha_shape(pcd, alpha)
mesh.compute_vertex_normals()
# 4. Сохранение
o3d.io.write_triangle_mesh("chair_model.obj", mesh)
print("Модель сохранена как chair_model.obj")
Требования к отчету
Код: Ссылка на Colab ноутбук с работающим циклом обработки N изображений.
Скриншоты:
Визуализация облака точек после 2-х фото.
Визуализация финального облака точек (после всех фото).
Скриншот полученного меша (
.obj), открытого в стандартном 3D-просмотрщике Windows/Mac или в Blender.
Анализ:
Как меняется количество точек при добавлении новых кадров?
Почему параметр
alphaпри создании меша сильно влияет на результат? (Что происходит, еслиalphaслишком большой или слишком маленький?)